Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c22f9dbf2b | ||
|
|
27a1b09d62 | ||
|
|
7bbb693924 | ||
|
|
9661fd5d91 | ||
|
|
d9ab583879 | ||
|
|
5d41bda997 | ||
|
|
3d037083c6 | ||
|
|
a3e440c254 | ||
|
|
adc44b99d3 | ||
|
|
60afeb4cfd | ||
|
|
02ff8b1a96 | ||
|
|
2156df22c6 | ||
|
|
cd2a3fac55 | ||
|
|
6300a3588a | ||
|
|
45213103e4 | ||
|
|
91b8b424d6 | ||
|
|
0d1c9277e5 | ||
|
|
db16d26103 | ||
|
|
0eb64d0975 | ||
|
|
39e503ae18 | ||
|
|
70ed354c42 | ||
|
|
ba98ae37f4 | ||
|
|
906d39793f | ||
|
|
f970c1928d | ||
|
|
2a1d966b87 | ||
|
|
a393b62e9f | ||
|
|
1247f72af6 | ||
|
|
6735bf252c | ||
|
|
508066d39f | ||
|
|
70956c204e | ||
|
|
16a7eac0c6 | ||
|
|
37ac08b182 | ||
|
|
5ef80b362e |
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,15 +1,28 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
Liste des évolutions du projet inventory
|
## [1.7.0] - 2026-03-02
|
||||||
|
|
||||||
## [0.0.0]
|
### Ajouts
|
||||||
### Parameters
|
- **Systeme de commentaires / tickets** : les utilisateurs peuvent laisser des commentaires sur les fiches (machines, pieces, composants, produits, categories, squelettes). Les gestionnaires peuvent les resoudre.
|
||||||
Ajouter dans le fichier .env
|
- **Page commentaires** (`/comments`) : vue centralisee avec filtres (statut, type d'entite), pagination et liens cliquables vers les fiches.
|
||||||
- DEFAULT_URI
|
- **Badge notifications** : compteur de commentaires ouverts sur l'avatar utilisateur et dans le menu profil (polling 60s).
|
||||||
- DATABASE_URL
|
- **Controle d'acces par roles** : ROLE_ADMIN, ROLE_GESTIONNAIRE, ROLE_VIEWER avec permissions granulaires sur toutes les pages.
|
||||||
|
- **Badge de role** dans le dropdown du profil utilisateur.
|
||||||
|
- **Journal d'audit etendu** : audit logging sur machines, constructeurs, types de modeles, documents et conversions.
|
||||||
|
- **Commande `app:init-profile-passwords`** : initialisation en masse des mots de passe et roles.
|
||||||
|
|
||||||
### Added
|
### Corrections
|
||||||
|
- Toggle switch pour les champs personnalises booleens (remplace les checkboxes).
|
||||||
|
- Recherche constructeur : filtrage cote client au lieu d'appels API debounce.
|
||||||
|
- Prevention des doublons de noms de constructeurs et de references de pieces (contraintes unique).
|
||||||
|
- Fix creation de squelettes machines : pagination, duplication, champs personnalises.
|
||||||
|
|
||||||
### Changed
|
### Migration requise
|
||||||
|
```bash
|
||||||
|
docker compose exec php php bin/console doctrine:migrations:migrate
|
||||||
|
docker compose exec php php bin/console app:init-profile-passwords
|
||||||
|
```
|
||||||
|
|
||||||
### Fixed
|
## [1.6.0] - 2026-02-xx
|
||||||
|
|
||||||
|
- Version initiale avec gestion du parc machines, pieces, composants, produits et categories.
|
||||||
|
|||||||
Submodule Inventory_frontend updated: 67af3c9c46...a98ab8c275
@@ -1,7 +1,9 @@
|
|||||||
api_platform:
|
api_platform:
|
||||||
title: Hello API Platform
|
title: Hello API Platform
|
||||||
version: 1.3.0
|
version: 1.4.0
|
||||||
defaults:
|
defaults:
|
||||||
stateless: false
|
stateless: false
|
||||||
cache_headers:
|
cache_headers:
|
||||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||||
|
pagination_items_per_page: 30
|
||||||
|
pagination_maximum_items_per_page: 200
|
||||||
|
|||||||
@@ -29,33 +29,36 @@ security:
|
|||||||
success_handler: lexik_jwt_authentication.handler.authentication_success
|
success_handler: lexik_jwt_authentication.handler.authentication_success
|
||||||
failure_handler: lexik_jwt_authentication.handler.authentication_failure
|
failure_handler: lexik_jwt_authentication.handler.authentication_failure
|
||||||
|
|
||||||
session_profile:
|
session_public:
|
||||||
pattern: ^/api/session
|
pattern: ^/api/session/profiles?$
|
||||||
stateless: false
|
security: false
|
||||||
|
|
||||||
session_api:
|
|
||||||
pattern: ^/api/(sites|machines|documents|profiles)
|
|
||||||
stateless: false
|
|
||||||
|
|
||||||
api:
|
api:
|
||||||
pattern: ^/api
|
pattern: ^/api
|
||||||
stateless: false
|
stateless: true
|
||||||
|
custom_authenticators:
|
||||||
|
- App\Security\SessionProfileAuthenticator
|
||||||
|
|
||||||
main:
|
main:
|
||||||
lazy: true
|
lazy: true
|
||||||
provider: app_user_provider
|
provider: app_user_provider
|
||||||
|
|
||||||
|
role_hierarchy:
|
||||||
|
ROLE_ADMIN: ROLE_GESTIONNAIRE
|
||||||
|
ROLE_GESTIONNAIRE: ROLE_VIEWER
|
||||||
|
ROLE_VIEWER: ROLE_USER
|
||||||
|
|
||||||
# Note: Only the *first* matching rule is applied
|
# Note: Only the *first* matching rule is applied
|
||||||
access_control:
|
access_control:
|
||||||
- { path: ^/api/session/profile, roles: PUBLIC_ACCESS }
|
- { path: ^/api/session/profile$, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/session/profiles, roles: PUBLIC_ACCESS }
|
- { path: ^/api/session/profiles, roles: PUBLIC_ACCESS, methods: [GET] }
|
||||||
- { path: ^/api, roles: PUBLIC_ACCESS }
|
- { path: ^/api/admin, roles: ROLE_ADMIN }
|
||||||
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/test, roles: PUBLIC_ACCESS }
|
- { path: ^/api/test, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/docs, roles: PUBLIC_ACCESS }
|
- { path: ^/docs, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/contexts, roles: PUBLIC_ACCESS }
|
- { path: ^/contexts, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/\.well-known, roles: PUBLIC_ACCESS }
|
- { path: ^/\.well-known, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
|
- { path: ^/api, roles: ROLE_VIEWER }
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
security:
|
security:
|
||||||
|
|||||||
@@ -45,34 +45,17 @@ services:
|
|||||||
- "${POSTGRES_PORT:-5433}:5432"
|
- "${POSTGRES_PORT:-5433}:5432"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
pgadmin:
|
adminer:
|
||||||
container_name: pgadmin-${DOCKER_APP_NAME}
|
container_name: adminer-${DOCKER_APP_NAME}
|
||||||
image: dpage/pgadmin4:latest
|
image: adminer:latest
|
||||||
user: root
|
|
||||||
environment:
|
environment:
|
||||||
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@admin.com}
|
ADMINER_DEFAULT_SERVER: db
|
||||||
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin}
|
ADMINER_DESIGN: dracula
|
||||||
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
|
||||||
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
|
|
||||||
PGADMIN_SERVER_JSON_FILE: '/pgadmin4/servers.json'
|
|
||||||
volumes:
|
|
||||||
- pgadmin_data:/var/lib/pgadmin
|
|
||||||
- ./docker/pgadmin/servers.json:/pgadmin4/servers.json:ro
|
|
||||||
- ./docker/pgadmin/pgpass:/pgadmin4/pgpass:ro
|
|
||||||
ports:
|
ports:
|
||||||
- "${PGADMIN_PORT:-5050}:80"
|
- "${ADMINER_PORT:-5050}:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
entrypoint: >
|
|
||||||
/bin/sh -c "
|
|
||||||
mkdir -p /var/lib/pgadmin &&
|
|
||||||
cp /pgadmin4/pgpass /var/lib/pgadmin/pgpass &&
|
|
||||||
chmod 600 /var/lib/pgadmin/pgpass &&
|
|
||||||
chown 5050:5050 /var/lib/pgadmin/pgpass &&
|
|
||||||
/entrypoint.sh
|
|
||||||
"
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pg_data:
|
pg_data:
|
||||||
pgadmin_data:
|
|
||||||
|
|||||||
51
migrations/Version20260302103003.php
Normal file
51
migrations/Version20260302103003.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260302103003 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create comments table + make piece reference unique instead of name';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// Comments table (IF NOT EXISTS in case first attempt partially succeeded)
|
||||||
|
$this->addSql('CREATE TABLE IF NOT EXISTS comments (id VARCHAR(36) NOT NULL, content TEXT NOT NULL, entity_type VARCHAR(50) NOT NULL, entity_id VARCHAR(36) NOT NULL, entity_name VARCHAR(255) DEFAULT NULL, author_id VARCHAR(36) NOT NULL, author_name VARCHAR(255) NOT NULL, status VARCHAR(20) NOT NULL, resolved_by_id VARCHAR(36) DEFAULT NULL, resolved_by_name VARCHAR(255) DEFAULT NULL, resolved_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comment_entity_status ON comments (entity_type, entity_id, status)');
|
||||||
|
$this->addSql('COMMENT ON COLUMN comments.resolved_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN comments.created_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN comments.updated_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
|
||||||
|
// Piece: remove unique on name
|
||||||
|
$this->addSql('DROP INDEX IF EXISTS uniq_b92d74725e237e06');
|
||||||
|
|
||||||
|
// Deduplicate piece references before adding unique constraint
|
||||||
|
$this->addSql("
|
||||||
|
UPDATE pieces p
|
||||||
|
SET reference = p.reference || '-' || LEFT(p.id, 6)
|
||||||
|
FROM (
|
||||||
|
SELECT id, reference,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY reference ORDER BY createdat) AS rn
|
||||||
|
FROM pieces
|
||||||
|
WHERE reference IS NOT NULL AND reference != ''
|
||||||
|
) dup
|
||||||
|
WHERE p.id = dup.id AND dup.rn > 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS uniq_pieces_reference ON pieces (reference)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE IF EXISTS comments');
|
||||||
|
$this->addSql('DROP INDEX IF EXISTS uniq_pieces_reference');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_b92d74725e237e06 ON pieces (name)');
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/Command/InitProfilePasswordsCommand.php
Normal file
85
src/Command/InitProfilePasswordsCommand.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
|
||||||
|
use function count;
|
||||||
|
use function in_array;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:init-profile-passwords',
|
||||||
|
description: 'Initialize all profile passwords to first letter of firstName + "123"',
|
||||||
|
)]
|
||||||
|
class InitProfilePasswordsCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$all = $this->profiles->findAll();
|
||||||
|
|
||||||
|
if (0 === count($all)) {
|
||||||
|
$io->warning('Aucun profil trouvé.');
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promote first profile to ROLE_ADMIN if none exists
|
||||||
|
$hasAdmin = false;
|
||||||
|
foreach ($all as $profile) {
|
||||||
|
if (in_array('ROLE_ADMIN', $profile->getRoles(), true)) {
|
||||||
|
$hasAdmin = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$isFirst = true;
|
||||||
|
$count = 0;
|
||||||
|
foreach ($all as $profile) {
|
||||||
|
// Set password: first letter of firstName + "123"
|
||||||
|
$firstLetter = mb_strtoupper(mb_substr($profile->getFirstName(), 0, 1));
|
||||||
|
$plain = $firstLetter.'123';
|
||||||
|
$hashed = $this->passwordHasher->hashPassword($profile, $plain);
|
||||||
|
$profile->setPassword($hashed);
|
||||||
|
|
||||||
|
// Set roles: first profile → ADMIN, others → VIEWER (minimum to use the app)
|
||||||
|
if (!$hasAdmin && $isFirst) {
|
||||||
|
$profile->setRoles(['ROLE_ADMIN']);
|
||||||
|
$io->writeln(sprintf(' %s %s → mdp: %s — ROLE_ADMIN', $profile->getFirstName(), $profile->getLastName(), $plain));
|
||||||
|
$isFirst = false;
|
||||||
|
} elseif (in_array('ROLE_USER', $profile->getRoles(), true) && !in_array('ROLE_VIEWER', $profile->getRoles(), true) && !in_array('ROLE_GESTIONNAIRE', $profile->getRoles(), true) && !in_array('ROLE_ADMIN', $profile->getRoles(), true)) {
|
||||||
|
$profile->setRoles(['ROLE_VIEWER']);
|
||||||
|
$io->writeln(sprintf(' %s %s → mdp: %s — ROLE_VIEWER', $profile->getFirstName(), $profile->getLastName(), $plain));
|
||||||
|
} else {
|
||||||
|
$io->writeln(sprintf(' %s %s → mdp: %s — %s', $profile->getFirstName(), $profile->getLastName(), $plain, implode(', ', $profile->getRoles())));
|
||||||
|
}
|
||||||
|
|
||||||
|
++$count;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
$io->success(sprintf('%d mot(s) de passe initialisé(s).', $count));
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/Controller/ActivityLogController.php
Normal file
90
src/Controller/ActivityLogController.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\AuditLogRepository;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class ActivityLogController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AuditLogRepository $auditLogs,
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/activity-logs', name: 'api_activity_logs', methods: ['GET'])]
|
||||||
|
public function __invoke(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$page = max(1, $request->query->getInt('page', 1));
|
||||||
|
$itemsPerPage = min(100, max(1, $request->query->getInt('itemsPerPage', 30)));
|
||||||
|
|
||||||
|
$filters = [];
|
||||||
|
if ($entityType = $request->query->get('entityType')) {
|
||||||
|
$filters['entityType'] = $entityType;
|
||||||
|
}
|
||||||
|
if ($action = $request->query->get('action')) {
|
||||||
|
$filters['action'] = $action;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->auditLogs->findAllPaginated($page, $itemsPerPage, $filters);
|
||||||
|
|
||||||
|
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn ($log) => $log->getActorProfileId(),
|
||||||
|
$result['items'],
|
||||||
|
))));
|
||||||
|
|
||||||
|
$actorMap = [];
|
||||||
|
if ([] !== $actorIds) {
|
||||||
|
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||||
|
foreach ($profiles as $profile) {
|
||||||
|
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
|
if ('' === $label) {
|
||||||
|
$label = $profile->getEmail() ?? $profile->getId();
|
||||||
|
}
|
||||||
|
$actorMap[$profile->getId()] = $label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array_map(
|
||||||
|
static function ($log) use ($actorMap) {
|
||||||
|
$actorId = $log->getActorProfileId();
|
||||||
|
$snapshot = $log->getSnapshot();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $log->getId(),
|
||||||
|
'entityType' => $log->getEntityType(),
|
||||||
|
'entityId' => $log->getEntityId(),
|
||||||
|
'entityName' => $snapshot['name'] ?? null,
|
||||||
|
'entityRef' => $snapshot['reference'] ?? null,
|
||||||
|
'action' => $log->getAction(),
|
||||||
|
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
'actor' => $actorId
|
||||||
|
? [
|
||||||
|
'id' => $actorId,
|
||||||
|
'label' => $actorMap[$actorId] ?? $actorId,
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
'diff' => $log->getDiff(),
|
||||||
|
'snapshot' => $snapshot,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
$result['items'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'items' => array_values($items),
|
||||||
|
'total' => $result['total'],
|
||||||
|
'page' => $page,
|
||||||
|
'itemsPerPage' => $itemsPerPage,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
193
src/Controller/AdminProfileController.php
Normal file
193
src/Controller/AdminProfileController.php
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
use function count;
|
||||||
|
use function in_array;
|
||||||
|
|
||||||
|
#[Route('/api/admin/profiles')]
|
||||||
|
final class AdminProfileController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('', name: 'admin_profiles_list', methods: ['GET'])]
|
||||||
|
public function list(): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
|
$items = $this->profiles->findBy([], ['firstName' => 'ASC']);
|
||||||
|
|
||||||
|
return new JsonResponse(array_map([$this, 'serializeProfile'], $items));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('', name: 'admin_profiles_create', methods: ['POST'])]
|
||||||
|
public function create(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
|
$payload = $request->toArray();
|
||||||
|
$firstName = trim((string) ($payload['firstName'] ?? ''));
|
||||||
|
$lastName = trim((string) ($payload['lastName'] ?? ''));
|
||||||
|
|
||||||
|
if ('' === $firstName || '' === $lastName) {
|
||||||
|
return new JsonResponse(['message' => 'firstName et lastName sont requis.'], JsonResponse::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = trim((string) ($payload['email'] ?? ''));
|
||||||
|
$password = $payload['password'] ?? null;
|
||||||
|
$role = $payload['role'] ?? 'ROLE_VIEWER';
|
||||||
|
|
||||||
|
$allowedRoles = ['ROLE_ADMIN', 'ROLE_GESTIONNAIRE', 'ROLE_VIEWER', 'ROLE_USER'];
|
||||||
|
if (!in_array($role, $allowedRoles, true)) {
|
||||||
|
return new JsonResponse(['message' => 'Role invalide.'], JsonResponse::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile = new Profile();
|
||||||
|
$profile->setFirstName($firstName);
|
||||||
|
$profile->setLastName($lastName);
|
||||||
|
$profile->setIsActive(true);
|
||||||
|
$profile->setRoles([$role]);
|
||||||
|
|
||||||
|
if ('' !== $email) {
|
||||||
|
$profile->setEmail($email);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $password && '' !== $password) {
|
||||||
|
$profile->setPassword(
|
||||||
|
$this->passwordHasher->hashPassword($profile, $password)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->persist($profile);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new JsonResponse($this->serializeProfile($profile), JsonResponse::HTTP_CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/role', name: 'admin_profiles_update_role', methods: ['PUT'])]
|
||||||
|
public function updateRole(string $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
|
$profile = $this->profiles->find($id);
|
||||||
|
if (!$profile) {
|
||||||
|
return new JsonResponse(['message' => 'Profil introuvable.'], JsonResponse::HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $request->toArray();
|
||||||
|
$role = $payload['role'] ?? null;
|
||||||
|
|
||||||
|
$allowedRoles = ['ROLE_ADMIN', 'ROLE_GESTIONNAIRE', 'ROLE_VIEWER', 'ROLE_USER'];
|
||||||
|
if (!$role || !in_array($role, $allowedRoles, true)) {
|
||||||
|
return new JsonResponse(['message' => 'Role invalide.'], JsonResponse::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent removing the last admin
|
||||||
|
if (in_array('ROLE_ADMIN', $profile->getRoles(), true) && 'ROLE_ADMIN' !== $role) {
|
||||||
|
$adminCount = $this->countAdmins();
|
||||||
|
if ($adminCount <= 1) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Impossible de retirer le dernier administrateur.'],
|
||||||
|
JsonResponse::HTTP_CONFLICT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile->setRoles([$role]);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new JsonResponse($this->serializeProfile($profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/password', name: 'admin_profiles_update_password', methods: ['PUT'])]
|
||||||
|
public function updatePassword(string $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
|
$profile = $this->profiles->find($id);
|
||||||
|
if (!$profile) {
|
||||||
|
return new JsonResponse(['message' => 'Profil introuvable.'], JsonResponse::HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $request->toArray();
|
||||||
|
$password = $payload['password'] ?? '';
|
||||||
|
|
||||||
|
if ('' === $password) {
|
||||||
|
return new JsonResponse(['message' => 'Le mot de passe est requis.'], JsonResponse::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile->setPassword(
|
||||||
|
$this->passwordHasher->hashPassword($profile, $password)
|
||||||
|
);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new JsonResponse($this->serializeProfile($profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/deactivate', name: 'admin_profiles_deactivate', methods: ['PUT'])]
|
||||||
|
public function deactivate(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
|
$profile = $this->profiles->find($id);
|
||||||
|
if (!$profile) {
|
||||||
|
return new JsonResponse(['message' => 'Profil introuvable.'], JsonResponse::HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent deactivating the last admin
|
||||||
|
if (in_array('ROLE_ADMIN', $profile->getRoles(), true)) {
|
||||||
|
$adminCount = $this->countAdmins();
|
||||||
|
if ($adminCount <= 1) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Impossible de desactiver le dernier administrateur.'],
|
||||||
|
JsonResponse::HTTP_CONFLICT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile->setIsActive(false);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new JsonResponse($this->serializeProfile($profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function serializeProfile(Profile $profile): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $profile->getId(),
|
||||||
|
'firstName' => $profile->getFirstName(),
|
||||||
|
'lastName' => $profile->getLastName(),
|
||||||
|
'email' => $profile->getEmail(),
|
||||||
|
'isActive' => $profile->isActive(),
|
||||||
|
'hasPassword' => null !== $profile->getPassword() && '' !== $profile->getPassword(),
|
||||||
|
'roles' => $profile->getRoles(),
|
||||||
|
'createdAt' => $profile->getCreatedAt()->format('c'),
|
||||||
|
'updatedAt' => $profile->getUpdatedAt()->format('c'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function countAdmins(): int
|
||||||
|
{
|
||||||
|
$all = $this->profiles->findBy(['isActive' => true]);
|
||||||
|
|
||||||
|
return count(array_filter(
|
||||||
|
$all,
|
||||||
|
static fn (Profile $p) => in_array('ROLE_ADMIN', $p->getRoles(), true)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/Controller/CommentController.php
Normal file
145
src/Controller/CommentController.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Comment;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route('/api/comments')]
|
||||||
|
final class CommentController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('', name: 'api_comments_create', methods: ['POST'])]
|
||||||
|
public function create(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$session = $request->getSession();
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if (!$profileId) {
|
||||||
|
return $this->json(['message' => 'Aucun profil actif.'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile = $this->profiles->find($profileId);
|
||||||
|
if (!$profile) {
|
||||||
|
return $this->json(['message' => 'Profil introuvable.'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = json_decode($request->getContent(), true);
|
||||||
|
if (!is_array($payload)) {
|
||||||
|
return $this->json(['message' => 'Payload JSON invalide.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = trim((string) ($payload['content'] ?? ''));
|
||||||
|
$entityType = trim((string) ($payload['entityType'] ?? ''));
|
||||||
|
$entityId = trim((string) ($payload['entityId'] ?? ''));
|
||||||
|
$entityName = isset($payload['entityName']) ? trim((string) $payload['entityName']) : null;
|
||||||
|
|
||||||
|
if ('' === $content) {
|
||||||
|
return $this->json(['message' => 'Le contenu est requis.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedTypes = ['machine', 'piece', 'composant', 'product', 'piece_category', 'component_category', 'product_category', 'machine_skeleton'];
|
||||||
|
if (!in_array($entityType, $allowedTypes, true)) {
|
||||||
|
return $this->json(['message' => 'Type d\'entité invalide.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('' === $entityId) {
|
||||||
|
return $this->json(['message' => 'L\'identifiant de l\'entité est requis.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$authorName = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
|
if ('' === $authorName) {
|
||||||
|
$authorName = $profile->getEmail() ?? 'Inconnu';
|
||||||
|
}
|
||||||
|
|
||||||
|
$comment = new Comment();
|
||||||
|
$comment->setContent($content);
|
||||||
|
$comment->setEntityType($entityType);
|
||||||
|
$comment->setEntityId($entityId);
|
||||||
|
$comment->setEntityName($entityName);
|
||||||
|
$comment->setAuthorId($profileId);
|
||||||
|
$comment->setAuthorName($authorName);
|
||||||
|
|
||||||
|
$this->entityManager->persist($comment);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this->json($this->normalize($comment), 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/resolve', name: 'api_comments_resolve', methods: ['PATCH'])]
|
||||||
|
public function resolve(string $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
|
$comment = $this->entityManager->getRepository(Comment::class)->find($id);
|
||||||
|
if (!$comment) {
|
||||||
|
return $this->json(['message' => 'Commentaire introuvable.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$session = $request->getSession();
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
$profile = $profileId ? $this->profiles->find($profileId) : null;
|
||||||
|
|
||||||
|
$resolverName = 'Inconnu';
|
||||||
|
if ($profile) {
|
||||||
|
$resolverName = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
|
if ('' === $resolverName) {
|
||||||
|
$resolverName = $profile->getEmail() ?? 'Inconnu';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$comment->setStatus('resolved');
|
||||||
|
$comment->setResolvedById($profileId);
|
||||||
|
$comment->setResolvedByName($resolverName);
|
||||||
|
$comment->setResolvedAt(new DateTimeImmutable());
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this->json($this->normalize($comment));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/stats/unresolved-count', name: 'api_comments_unresolved_count', methods: ['GET'])]
|
||||||
|
public function unresolvedCount(): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$count = $this->entityManager->getRepository(Comment::class)
|
||||||
|
->count(['status' => 'open'])
|
||||||
|
;
|
||||||
|
|
||||||
|
return $this->json(['count' => $count]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalize(Comment $comment): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $comment->getId(),
|
||||||
|
'content' => $comment->getContent(),
|
||||||
|
'entityType' => $comment->getEntityType(),
|
||||||
|
'entityId' => $comment->getEntityId(),
|
||||||
|
'entityName' => $comment->getEntityName(),
|
||||||
|
'authorId' => $comment->getAuthorId(),
|
||||||
|
'authorName' => $comment->getAuthorName(),
|
||||||
|
'status' => $comment->getStatus(),
|
||||||
|
'resolvedById' => $comment->getResolvedById(),
|
||||||
|
'resolvedByName' => $comment->getResolvedByName(),
|
||||||
|
'resolvedAt' => $comment->getResolvedAt()?->format(DateTimeInterface::ATOM),
|
||||||
|
'createdAt' => $comment->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
'updatedAt' => $comment->getUpdatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,22 +7,25 @@ namespace App\Controller;
|
|||||||
use App\Repository\AuditLogRepository;
|
use App\Repository\AuditLogRepository;
|
||||||
use App\Repository\ComposantRepository;
|
use App\Repository\ComposantRepository;
|
||||||
use App\Repository\ProfileRepository;
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
final class ComposantHistoryController
|
final class ComposantHistoryController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ComposantRepository $components,
|
private readonly ComposantRepository $components,
|
||||||
private readonly AuditLogRepository $auditLogs,
|
private readonly AuditLogRepository $auditLogs,
|
||||||
private readonly ProfileRepository $profiles,
|
private readonly ProfileRepository $profiles,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/api/composants/{id}/history', name: 'api_composant_history', methods: ['GET'])]
|
#[Route('/api/composants/{id}/history', name: 'api_composant_history', methods: ['GET'])]
|
||||||
public function __invoke(string $id): JsonResponse
|
public function __invoke(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
$component = $this->components->find($id);
|
$component = $this->components->find($id);
|
||||||
if (!$component) {
|
if (!$component) {
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
@@ -39,11 +42,11 @@ final class ComposantHistoryController
|
|||||||
))));
|
))));
|
||||||
|
|
||||||
$actorMap = [];
|
$actorMap = [];
|
||||||
if ($actorIds !== []) {
|
if ([] !== $actorIds) {
|
||||||
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||||
foreach ($profiles as $profile) {
|
foreach ($profiles as $profile) {
|
||||||
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
if ($label === '') {
|
if ('' === $label) {
|
||||||
$label = $profile->getEmail() ?? $profile->getId();
|
$label = $profile->getEmail() ?? $profile->getId();
|
||||||
}
|
}
|
||||||
$actorMap[$profile->getId()] = $label;
|
$actorMap[$profile->getId()] = $label;
|
||||||
@@ -55,16 +58,16 @@ final class ComposantHistoryController
|
|||||||
$actorId = $log->getActorProfileId();
|
$actorId = $log->getActorProfileId();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $log->getId(),
|
'id' => $log->getId(),
|
||||||
'action' => $log->getAction(),
|
'action' => $log->getAction(),
|
||||||
'createdAt' => $log->getCreatedAt()->format(\DateTimeInterface::ATOM),
|
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
'actor' => $actorId
|
'actor' => $actorId
|
||||||
? [
|
? [
|
||||||
'id' => $actorId,
|
'id' => $actorId,
|
||||||
'label' => $actorMap[$actorId] ?? $actorId,
|
'label' => $actorMap[$actorId] ?? $actorId,
|
||||||
]
|
]
|
||||||
: null,
|
: null,
|
||||||
'diff' => $log->getDiff(),
|
'diff' => $log->getDiff(),
|
||||||
'snapshot' => $log->getSnapshot(),
|
'snapshot' => $log->getSnapshot(),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@@ -77,4 +80,3 @@ final class ComposantHistoryController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,13 @@ class CustomFieldValueController extends AbstractController
|
|||||||
private readonly ComposantRepository $composantRepository,
|
private readonly ComposantRepository $composantRepository,
|
||||||
private readonly PieceRepository $pieceRepository,
|
private readonly PieceRepository $pieceRepository,
|
||||||
private readonly ProductRepository $productRepository,
|
private readonly ProductRepository $productRepository,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('', name: 'custom_field_values_create', methods: ['POST'])]
|
#[Route('', name: 'custom_field_values_create', methods: ['POST'])]
|
||||||
public function create(Request $request): JsonResponse
|
public function create(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
$payload = $this->decodePayload($request);
|
$payload = $this->decodePayload($request);
|
||||||
if ($payload instanceof JsonResponse) {
|
if ($payload instanceof JsonResponse) {
|
||||||
return $payload;
|
return $payload;
|
||||||
@@ -64,6 +65,8 @@ class CustomFieldValueController extends AbstractController
|
|||||||
#[Route('/upsert', name: 'custom_field_values_upsert', methods: ['POST'])]
|
#[Route('/upsert', name: 'custom_field_values_upsert', methods: ['POST'])]
|
||||||
public function upsert(Request $request): JsonResponse
|
public function upsert(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
$payload = $this->decodePayload($request);
|
$payload = $this->decodePayload($request);
|
||||||
if ($payload instanceof JsonResponse) {
|
if ($payload instanceof JsonResponse) {
|
||||||
return $payload;
|
return $payload;
|
||||||
@@ -80,7 +83,7 @@ class CustomFieldValueController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$existing = $this->customFieldValueRepository->findOneBy([
|
$existing = $this->customFieldValueRepository->findOneBy([
|
||||||
'customField' => $customField,
|
'customField' => $customField,
|
||||||
$target['type'] => $target['entity'],
|
$target['type'] => $target['entity'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -105,9 +108,11 @@ class CustomFieldValueController extends AbstractController
|
|||||||
#[Route('/{entityType}/{entityId}', name: 'custom_field_values_list', methods: ['GET'])]
|
#[Route('/{entityType}/{entityId}', name: 'custom_field_values_list', methods: ['GET'])]
|
||||||
public function listByEntity(string $entityType, string $entityId): JsonResponse
|
public function listByEntity(string $entityType, string $entityId): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
$target = $this->resolveTarget([
|
$target = $this->resolveTarget([
|
||||||
'entityType' => $entityType,
|
'entityType' => $entityType,
|
||||||
'entityId' => $entityId,
|
'entityId' => $entityId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($target instanceof JsonResponse) {
|
if ($target instanceof JsonResponse) {
|
||||||
@@ -127,6 +132,8 @@ class CustomFieldValueController extends AbstractController
|
|||||||
#[Route('/{id}', name: 'custom_field_values_update', methods: ['PATCH'])]
|
#[Route('/{id}', name: 'custom_field_values_update', methods: ['PATCH'])]
|
||||||
public function update(string $id, Request $request): JsonResponse
|
public function update(string $id, Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
$value = $this->customFieldValueRepository->find($id);
|
$value = $this->customFieldValueRepository->find($id);
|
||||||
if (!$value instanceof CustomFieldValue) {
|
if (!$value instanceof CustomFieldValue) {
|
||||||
return $this->json(['success' => false, 'error' => 'Custom field value not found.'], 404);
|
return $this->json(['success' => false, 'error' => 'Custom field value not found.'], 404);
|
||||||
@@ -149,6 +156,8 @@ class CustomFieldValueController extends AbstractController
|
|||||||
#[Route('/{id}', name: 'custom_field_values_delete', methods: ['DELETE'])]
|
#[Route('/{id}', name: 'custom_field_values_delete', methods: ['DELETE'])]
|
||||||
public function delete(string $id): JsonResponse
|
public function delete(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
$value = $this->customFieldValueRepository->find($id);
|
$value = $this->customFieldValueRepository->find($id);
|
||||||
if (!$value instanceof CustomFieldValue) {
|
if (!$value instanceof CustomFieldValue) {
|
||||||
return $this->json(['success' => false, 'error' => 'Custom field value not found.'], 404);
|
return $this->json(['success' => false, 'error' => 'Custom field value not found.'], 404);
|
||||||
@@ -173,7 +182,7 @@ class CustomFieldValueController extends AbstractController
|
|||||||
private function resolveCustomField(array $payload): CustomField|JsonResponse
|
private function resolveCustomField(array $payload): CustomField|JsonResponse
|
||||||
{
|
{
|
||||||
$customFieldId = isset($payload['customFieldId']) ? trim((string) $payload['customFieldId']) : '';
|
$customFieldId = isset($payload['customFieldId']) ? trim((string) $payload['customFieldId']) : '';
|
||||||
if ($customFieldId !== '') {
|
if ('' !== $customFieldId) {
|
||||||
$customField = $this->customFieldRepository->find($customFieldId);
|
$customField = $this->customFieldRepository->find($customFieldId);
|
||||||
if ($customField instanceof CustomField) {
|
if ($customField instanceof CustomField) {
|
||||||
return $customField;
|
return $customField;
|
||||||
@@ -183,7 +192,7 @@ class CustomFieldValueController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$customFieldName = isset($payload['customFieldName']) ? trim((string) $payload['customFieldName']) : '';
|
$customFieldName = isset($payload['customFieldName']) ? trim((string) $payload['customFieldName']) : '';
|
||||||
if ($customFieldName === '') {
|
if ('' === $customFieldName) {
|
||||||
return $this->json(['success' => false, 'error' => 'customFieldId or customFieldName is required.'], 400);
|
return $this->json(['success' => false, 'error' => 'customFieldId or customFieldName is required.'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,30 +214,31 @@ class CustomFieldValueController extends AbstractController
|
|||||||
private function resolveTarget(array $payload): array|JsonResponse
|
private function resolveTarget(array $payload): array|JsonResponse
|
||||||
{
|
{
|
||||||
$entityType = isset($payload['entityType']) ? strtolower((string) $payload['entityType']) : '';
|
$entityType = isset($payload['entityType']) ? strtolower((string) $payload['entityType']) : '';
|
||||||
$entityId = isset($payload['entityId']) ? trim((string) $payload['entityId']) : '';
|
$entityId = isset($payload['entityId']) ? trim((string) $payload['entityId']) : '';
|
||||||
|
|
||||||
if ($entityType === '' || $entityId === '') {
|
if ('' === $entityType || '' === $entityId) {
|
||||||
foreach (['machine', 'composant', 'piece', 'product'] as $candidate) {
|
foreach (['machine', 'composant', 'piece', 'product'] as $candidate) {
|
||||||
$key = $candidate . 'Id';
|
$key = $candidate.'Id';
|
||||||
if (!isset($payload[$key])) {
|
if (!isset($payload[$key])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$entityType = $candidate;
|
$entityType = $candidate;
|
||||||
$entityId = trim((string) $payload[$key]);
|
$entityId = trim((string) $payload[$key]);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($entityType === '' || $entityId === '') {
|
if ('' === $entityType || '' === $entityId) {
|
||||||
return $this->json(['success' => false, 'error' => 'Entity target is missing.'], 400);
|
return $this->json(['success' => false, 'error' => 'Entity target is missing.'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
return match ($entityType) {
|
return match ($entityType) {
|
||||||
'machine' => $this->resolveEntity('machine', $entityId, $this->machineRepository),
|
'machine' => $this->resolveEntity('machine', $entityId, $this->machineRepository),
|
||||||
'composant' => $this->resolveEntity('composant', $entityId, $this->composantRepository),
|
'composant' => $this->resolveEntity('composant', $entityId, $this->composantRepository),
|
||||||
'piece' => $this->resolveEntity('piece', $entityId, $this->pieceRepository),
|
'piece' => $this->resolveEntity('piece', $entityId, $this->pieceRepository),
|
||||||
'product' => $this->resolveEntity('product', $entityId, $this->productRepository),
|
'product' => $this->resolveEntity('product', $entityId, $this->productRepository),
|
||||||
default => $this->json(['success' => false, 'error' => 'Unsupported entity type.'], 400),
|
default => $this->json(['success' => false, 'error' => 'Unsupported entity type.'], 400),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,15 +257,22 @@ class CustomFieldValueController extends AbstractController
|
|||||||
switch ($type) {
|
switch ($type) {
|
||||||
case 'machine':
|
case 'machine':
|
||||||
$value->setMachine($entity);
|
$value->setMachine($entity);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'composant':
|
case 'composant':
|
||||||
$value->setComposant($entity);
|
$value->setComposant($entity);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'piece':
|
case 'piece':
|
||||||
$value->setPiece($entity);
|
$value->setPiece($entity);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'product':
|
case 'product':
|
||||||
$value->setProduct($entity);
|
$value->setProduct($entity);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,23 +282,23 @@ class CustomFieldValueController extends AbstractController
|
|||||||
$customField = $value->getCustomField();
|
$customField = $value->getCustomField();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $value->getId(),
|
'id' => $value->getId(),
|
||||||
'value' => $value->getValue(),
|
'value' => $value->getValue(),
|
||||||
'customFieldId' => $customField->getId(),
|
'customFieldId' => $customField->getId(),
|
||||||
'customField' => [
|
'customField' => [
|
||||||
'id' => $customField->getId(),
|
'id' => $customField->getId(),
|
||||||
'name' => $customField->getName(),
|
'name' => $customField->getName(),
|
||||||
'type' => $customField->getType(),
|
'type' => $customField->getType(),
|
||||||
'required' => $customField->isRequired(),
|
'required' => $customField->isRequired(),
|
||||||
'options' => $customField->getOptions(),
|
'options' => $customField->getOptions(),
|
||||||
'orderIndex' => $customField->getOrderIndex(),
|
'orderIndex' => $customField->getOrderIndex(),
|
||||||
],
|
],
|
||||||
'machineId' => $value->getMachine()?->getId(),
|
'machineId' => $value->getMachine()?->getId(),
|
||||||
'composantId' => $value->getComposant()?->getId(),
|
'composantId' => $value->getComposant()?->getId(),
|
||||||
'pieceId' => $value->getPiece()?->getId(),
|
'pieceId' => $value->getPiece()?->getId(),
|
||||||
'productId' => $value->getProduct()?->getId(),
|
'productId' => $value->getProduct()?->getId(),
|
||||||
'createdAt' => $value->getCreatedAt()->format(DATE_ATOM),
|
'createdAt' => $value->getCreatedAt()->format(DATE_ATOM),
|
||||||
'updatedAt' => $value->getUpdatedAt()->format(DATE_ATOM),
|
'updatedAt' => $value->getUpdatedAt()->format(DATE_ATOM),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,12 +25,13 @@ class DocumentQueryController extends AbstractController
|
|||||||
private readonly ComposantRepository $composantRepository,
|
private readonly ComposantRepository $composantRepository,
|
||||||
private readonly PieceRepository $pieceRepository,
|
private readonly PieceRepository $pieceRepository,
|
||||||
private readonly ProductRepository $productRepository,
|
private readonly ProductRepository $productRepository,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/site/{id}', name: 'documents_by_site', methods: ['GET'])]
|
#[Route('/site/{id}', name: 'documents_by_site', methods: ['GET'])]
|
||||||
public function listBySite(string $id): JsonResponse
|
public function listBySite(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
$site = $this->siteRepository->find($id);
|
$site = $this->siteRepository->find($id);
|
||||||
if (!$site) {
|
if (!$site) {
|
||||||
return $this->json(['success' => false, 'error' => 'Site not found.'], 404);
|
return $this->json(['success' => false, 'error' => 'Site not found.'], 404);
|
||||||
@@ -44,6 +45,8 @@ class DocumentQueryController extends AbstractController
|
|||||||
#[Route('/machine/{id}', name: 'documents_by_machine', methods: ['GET'])]
|
#[Route('/machine/{id}', name: 'documents_by_machine', methods: ['GET'])]
|
||||||
public function listByMachine(string $id): JsonResponse
|
public function listByMachine(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
$machine = $this->machineRepository->find($id);
|
$machine = $this->machineRepository->find($id);
|
||||||
if (!$machine) {
|
if (!$machine) {
|
||||||
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
||||||
@@ -57,6 +60,8 @@ class DocumentQueryController extends AbstractController
|
|||||||
#[Route('/composant/{id}', name: 'documents_by_composant', methods: ['GET'])]
|
#[Route('/composant/{id}', name: 'documents_by_composant', methods: ['GET'])]
|
||||||
public function listByComposant(string $id): JsonResponse
|
public function listByComposant(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
$composant = $this->composantRepository->find($id);
|
$composant = $this->composantRepository->find($id);
|
||||||
if (!$composant) {
|
if (!$composant) {
|
||||||
return $this->json(['success' => false, 'error' => 'Composant not found.'], 404);
|
return $this->json(['success' => false, 'error' => 'Composant not found.'], 404);
|
||||||
@@ -70,6 +75,8 @@ class DocumentQueryController extends AbstractController
|
|||||||
#[Route('/piece/{id}', name: 'documents_by_piece', methods: ['GET'])]
|
#[Route('/piece/{id}', name: 'documents_by_piece', methods: ['GET'])]
|
||||||
public function listByPiece(string $id): JsonResponse
|
public function listByPiece(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
$piece = $this->pieceRepository->find($id);
|
$piece = $this->pieceRepository->find($id);
|
||||||
if (!$piece) {
|
if (!$piece) {
|
||||||
return $this->json(['success' => false, 'error' => 'Piece not found.'], 404);
|
return $this->json(['success' => false, 'error' => 'Piece not found.'], 404);
|
||||||
@@ -83,6 +90,8 @@ class DocumentQueryController extends AbstractController
|
|||||||
#[Route('/product/{id}', name: 'documents_by_product', methods: ['GET'])]
|
#[Route('/product/{id}', name: 'documents_by_product', methods: ['GET'])]
|
||||||
public function listByProduct(string $id): JsonResponse
|
public function listByProduct(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
$product = $this->productRepository->find($id);
|
$product = $this->productRepository->find($id);
|
||||||
if (!$product) {
|
if (!$product) {
|
||||||
return $this->json(['success' => false, 'error' => 'Product not found.'], 404);
|
return $this->json(['success' => false, 'error' => 'Product not found.'], 404);
|
||||||
@@ -100,19 +109,19 @@ class DocumentQueryController extends AbstractController
|
|||||||
{
|
{
|
||||||
return array_map(static function (Document $document): array {
|
return array_map(static function (Document $document): array {
|
||||||
return [
|
return [
|
||||||
'id' => $document->getId(),
|
'id' => $document->getId(),
|
||||||
'name' => $document->getName(),
|
'name' => $document->getName(),
|
||||||
'filename' => $document->getFilename(),
|
'filename' => $document->getFilename(),
|
||||||
'path' => $document->getPath(),
|
'path' => $document->getPath(),
|
||||||
'mimeType' => $document->getMimeType(),
|
'mimeType' => $document->getMimeType(),
|
||||||
'size' => $document->getSize(),
|
'size' => $document->getSize(),
|
||||||
'siteId' => $document->getSite()?->getId(),
|
'siteId' => $document->getSite()?->getId(),
|
||||||
'machineId' => $document->getMachine()?->getId(),
|
'machineId' => $document->getMachine()?->getId(),
|
||||||
'composantId' => $document->getComposant()?->getId(),
|
'composantId' => $document->getComposant()?->getId(),
|
||||||
'pieceId' => $document->getPiece()?->getId(),
|
'pieceId' => $document->getPiece()?->getId(),
|
||||||
'productId' => $document->getProduct()?->getId(),
|
'productId' => $document->getProduct()?->getId(),
|
||||||
'createdAt' => $document->getCreatedAt()->format(DATE_ATOM),
|
'createdAt' => $document->getCreatedAt()->format(DATE_ATOM),
|
||||||
'updatedAt' => $document->getUpdatedAt()->format(DATE_ATOM),
|
'updatedAt' => $document->getUpdatedAt()->format(DATE_ATOM),
|
||||||
];
|
];
|
||||||
}, $documents);
|
}, $documents);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ class MachineCustomFieldsController extends AbstractController
|
|||||||
private readonly EntityManagerInterface $entityManager,
|
private readonly EntityManagerInterface $entityManager,
|
||||||
private readonly MachineRepository $machineRepository,
|
private readonly MachineRepository $machineRepository,
|
||||||
private readonly CustomFieldValueRepository $customFieldValueRepository,
|
private readonly CustomFieldValueRepository $customFieldValueRepository,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/{id}/add-custom-fields', name: 'machine_add_custom_fields', methods: ['POST'])]
|
#[Route('/{id}/add-custom-fields', name: 'machine_add_custom_fields', methods: ['POST'])]
|
||||||
public function addMissingCustomFields(string $id): JsonResponse
|
public function addMissingCustomFields(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
$machine = $this->machineRepository->find($id);
|
$machine = $this->machineRepository->find($id);
|
||||||
if (!$machine instanceof Machine) {
|
if (!$machine instanceof Machine) {
|
||||||
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
||||||
@@ -42,7 +43,7 @@ class MachineCustomFieldsController extends AbstractController
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$existing = $this->customFieldValueRepository->findOneBy([
|
$existing = $this->customFieldValueRepository->findOneBy([
|
||||||
'machine' => $machine,
|
'machine' => $machine,
|
||||||
'customField' => $customField,
|
'customField' => $customField,
|
||||||
]);
|
]);
|
||||||
if ($existing instanceof CustomFieldValue) {
|
if ($existing instanceof CustomFieldValue) {
|
||||||
@@ -61,12 +62,12 @@ class MachineCustomFieldsController extends AbstractController
|
|||||||
$values = $this->customFieldValueRepository->findBy(['machine' => $machine]);
|
$values = $this->customFieldValueRepository->findBy(['machine' => $machine]);
|
||||||
|
|
||||||
return $this->json([
|
return $this->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'machineId' => $machine->getId(),
|
'machineId' => $machine->getId(),
|
||||||
'customFieldValues' => array_map(
|
'customFieldValues' => array_map(
|
||||||
static fn (CustomFieldValue $value) => [
|
static fn (CustomFieldValue $value) => [
|
||||||
'id' => $value->getId(),
|
'id' => $value->getId(),
|
||||||
'value' => $value->getValue(),
|
'value' => $value->getValue(),
|
||||||
'customFieldId' => $value->getCustomField()->getId(),
|
'customFieldId' => $value->getCustomField()->getId(),
|
||||||
],
|
],
|
||||||
$values
|
$values
|
||||||
|
|||||||
82
src/Controller/MachineHistoryController.php
Normal file
82
src/Controller/MachineHistoryController.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\AuditLogRepository;
|
||||||
|
use App\Repository\MachineRepository;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class MachineHistoryController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly MachineRepository $machines,
|
||||||
|
private readonly AuditLogRepository $auditLogs,
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/machines/{id}/history', name: 'api_machine_history', methods: ['GET'])]
|
||||||
|
public function __invoke(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$machine = $this->machines->find($id);
|
||||||
|
if (!$machine) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Machine introuvable.'],
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = $this->auditLogs->findEntityHistory('machine', $id, 200);
|
||||||
|
|
||||||
|
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn ($log) => $log->getActorProfileId(),
|
||||||
|
$logs,
|
||||||
|
))));
|
||||||
|
|
||||||
|
$actorMap = [];
|
||||||
|
if ([] !== $actorIds) {
|
||||||
|
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||||
|
foreach ($profiles as $profile) {
|
||||||
|
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
|
if ('' === $label) {
|
||||||
|
$label = $profile->getEmail() ?? $profile->getId();
|
||||||
|
}
|
||||||
|
$actorMap[$profile->getId()] = $label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array_map(
|
||||||
|
static function ($log) use ($actorMap) {
|
||||||
|
$actorId = $log->getActorProfileId();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $log->getId(),
|
||||||
|
'action' => $log->getAction(),
|
||||||
|
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
'actor' => $actorId
|
||||||
|
? [
|
||||||
|
'id' => $actorId,
|
||||||
|
'label' => $actorMap[$actorId] ?? $actorId,
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
'diff' => $log->getDiff(),
|
||||||
|
'snapshot' => $log->getSnapshot(),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
$logs,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'items' => array_values($items),
|
||||||
|
'total' => count($items),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\Composant;
|
use App\Entity\Composant;
|
||||||
|
use App\Entity\CustomField;
|
||||||
use App\Entity\Machine;
|
use App\Entity\Machine;
|
||||||
use App\Entity\MachineComponentLink;
|
use App\Entity\MachineComponentLink;
|
||||||
use App\Entity\MachinePieceLink;
|
use App\Entity\MachinePieceLink;
|
||||||
@@ -47,20 +48,21 @@ class MachineSkeletonController extends AbstractController
|
|||||||
private readonly TypeMachineComponentRequirementRepository $componentRequirementRepository,
|
private readonly TypeMachineComponentRequirementRepository $componentRequirementRepository,
|
||||||
private readonly TypeMachinePieceRequirementRepository $pieceRequirementRepository,
|
private readonly TypeMachinePieceRequirementRepository $pieceRequirementRepository,
|
||||||
private readonly TypeMachineProductRequirementRepository $productRequirementRepository,
|
private readonly TypeMachineProductRequirementRepository $productRequirementRepository,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/{id}/skeleton', name: 'machine_skeleton_get', methods: ['GET'])]
|
#[Route('/{id}/skeleton', name: 'machine_skeleton_get', methods: ['GET'])]
|
||||||
public function getSkeleton(string $id): JsonResponse
|
public function getSkeleton(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
$machine = $this->machineRepository->find($id);
|
$machine = $this->machineRepository->find($id);
|
||||||
if (!$machine instanceof Machine) {
|
if (!$machine instanceof Machine) {
|
||||||
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$componentLinks = $this->machineComponentLinkRepository->findBy(['machine' => $machine]);
|
$componentLinks = $this->machineComponentLinkRepository->findBy(['machine' => $machine]);
|
||||||
$pieceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $machine]);
|
$pieceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $machine]);
|
||||||
$productLinks = $this->machineProductLinkRepository->findBy(['machine' => $machine]);
|
$productLinks = $this->machineProductLinkRepository->findBy(['machine' => $machine]);
|
||||||
|
|
||||||
return $this->json($this->normalizeMachineSkeletonResponse(
|
return $this->json($this->normalizeMachineSkeletonResponse(
|
||||||
$machine,
|
$machine,
|
||||||
@@ -73,6 +75,8 @@ class MachineSkeletonController extends AbstractController
|
|||||||
#[Route('/{id}/skeleton', name: 'machine_skeleton_update', methods: ['PATCH'])]
|
#[Route('/{id}/skeleton', name: 'machine_skeleton_update', methods: ['PATCH'])]
|
||||||
public function updateSkeleton(string $id, Request $request): JsonResponse
|
public function updateSkeleton(string $id, Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
$machine = $this->machineRepository->find($id);
|
$machine = $this->machineRepository->find($id);
|
||||||
if (!$machine instanceof Machine) {
|
if (!$machine instanceof Machine) {
|
||||||
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
||||||
@@ -84,8 +88,8 @@ class MachineSkeletonController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$componentLinksPayload = $this->normalizePayloadList($payload['componentLinks'] ?? []);
|
$componentLinksPayload = $this->normalizePayloadList($payload['componentLinks'] ?? []);
|
||||||
$pieceLinksPayload = $this->normalizePayloadList($payload['pieceLinks'] ?? []);
|
$pieceLinksPayload = $this->normalizePayloadList($payload['pieceLinks'] ?? []);
|
||||||
$productLinksPayload = $this->normalizePayloadList($payload['productLinks'] ?? []);
|
$productLinksPayload = $this->normalizePayloadList($payload['productLinks'] ?? []);
|
||||||
|
|
||||||
$componentLinks = $this->applyComponentLinks($machine, $componentLinksPayload);
|
$componentLinks = $this->applyComponentLinks($machine, $componentLinksPayload);
|
||||||
if ($componentLinks instanceof JsonResponse) {
|
if ($componentLinks instanceof JsonResponse) {
|
||||||
@@ -117,19 +121,20 @@ class MachineSkeletonController extends AbstractController
|
|||||||
if (!is_array($value)) {
|
if (!is_array($value)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return array_values(array_filter($value, static fn ($item) => is_array($item)));
|
return array_values(array_filter($value, static fn ($item) => is_array($item)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function applyComponentLinks(Machine $machine, array $payload): array|JsonResponse
|
private function applyComponentLinks(Machine $machine, array $payload): array|JsonResponse
|
||||||
{
|
{
|
||||||
$existing = $this->indexLinksById($this->machineComponentLinkRepository->findBy(['machine' => $machine]));
|
$existing = $this->indexLinksById($this->machineComponentLinkRepository->findBy(['machine' => $machine]));
|
||||||
$keepIds = [];
|
$keepIds = [];
|
||||||
$pendingParents = [];
|
$pendingParents = [];
|
||||||
$links = [];
|
$links = [];
|
||||||
|
|
||||||
foreach ($payload as $entry) {
|
foreach ($payload as $entry) {
|
||||||
$linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
|
$linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
|
||||||
$link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineComponentLink();
|
$link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineComponentLink();
|
||||||
if (!$linkId) {
|
if (!$linkId) {
|
||||||
$linkId = $this->generateCuid();
|
$linkId = $this->generateCuid();
|
||||||
}
|
}
|
||||||
@@ -167,7 +172,7 @@ class MachineSkeletonController extends AbstractController
|
|||||||
|
|
||||||
$this->entityManager->persist($link);
|
$this->entityManager->persist($link);
|
||||||
$links[$linkId] = $link;
|
$links[$linkId] = $link;
|
||||||
$keepIds[] = $linkId;
|
$keepIds[] = $linkId;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($pendingParents as $linkId => $parentId) {
|
foreach ($pendingParents as $linkId => $parentId) {
|
||||||
@@ -190,15 +195,15 @@ class MachineSkeletonController extends AbstractController
|
|||||||
|
|
||||||
private function applyPieceLinks(Machine $machine, array $payload, array $componentLinks): array|JsonResponse
|
private function applyPieceLinks(Machine $machine, array $payload, array $componentLinks): array|JsonResponse
|
||||||
{
|
{
|
||||||
$existing = $this->indexLinksById($this->machinePieceLinkRepository->findBy(['machine' => $machine]));
|
$existing = $this->indexLinksById($this->machinePieceLinkRepository->findBy(['machine' => $machine]));
|
||||||
$componentIndex = $this->indexLinksById($componentLinks);
|
$componentIndex = $this->indexLinksById($componentLinks);
|
||||||
$keepIds = [];
|
$keepIds = [];
|
||||||
$pendingParents = [];
|
$pendingParents = [];
|
||||||
$links = [];
|
$links = [];
|
||||||
|
|
||||||
foreach ($payload as $entry) {
|
foreach ($payload as $entry) {
|
||||||
$linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
|
$linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
|
||||||
$link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachinePieceLink();
|
$link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachinePieceLink();
|
||||||
if (!$linkId) {
|
if (!$linkId) {
|
||||||
$linkId = $this->generateCuid();
|
$linkId = $this->generateCuid();
|
||||||
}
|
}
|
||||||
@@ -236,7 +241,7 @@ class MachineSkeletonController extends AbstractController
|
|||||||
|
|
||||||
$this->entityManager->persist($link);
|
$this->entityManager->persist($link);
|
||||||
$links[$linkId] = $link;
|
$links[$linkId] = $link;
|
||||||
$keepIds[] = $linkId;
|
$keepIds[] = $linkId;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($pendingParents as $linkId => $parentId) {
|
foreach ($pendingParents as $linkId => $parentId) {
|
||||||
@@ -263,16 +268,16 @@ class MachineSkeletonController extends AbstractController
|
|||||||
array $componentLinks,
|
array $componentLinks,
|
||||||
array $pieceLinks,
|
array $pieceLinks,
|
||||||
): array|JsonResponse {
|
): array|JsonResponse {
|
||||||
$existing = $this->indexLinksById($this->machineProductLinkRepository->findBy(['machine' => $machine]));
|
$existing = $this->indexLinksById($this->machineProductLinkRepository->findBy(['machine' => $machine]));
|
||||||
$componentIndex = $this->indexLinksById($componentLinks);
|
$componentIndex = $this->indexLinksById($componentLinks);
|
||||||
$pieceIndex = $this->indexLinksById($pieceLinks);
|
$pieceIndex = $this->indexLinksById($pieceLinks);
|
||||||
$keepIds = [];
|
$keepIds = [];
|
||||||
$pendingParents = [];
|
$pendingParents = [];
|
||||||
$links = [];
|
$links = [];
|
||||||
|
|
||||||
foreach ($payload as $entry) {
|
foreach ($payload as $entry) {
|
||||||
$linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
|
$linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
|
||||||
$link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineProductLink();
|
$link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineProductLink();
|
||||||
if (!$linkId) {
|
if (!$linkId) {
|
||||||
$linkId = $this->generateCuid();
|
$linkId = $this->generateCuid();
|
||||||
}
|
}
|
||||||
@@ -302,13 +307,13 @@ class MachineSkeletonController extends AbstractController
|
|||||||
|
|
||||||
$pendingParents[$linkId] = [
|
$pendingParents[$linkId] = [
|
||||||
'parentComponentLinkId' => $this->resolveIdentifier($entry, ['parentComponentLinkId']),
|
'parentComponentLinkId' => $this->resolveIdentifier($entry, ['parentComponentLinkId']),
|
||||||
'parentPieceLinkId' => $this->resolveIdentifier($entry, ['parentPieceLinkId']),
|
'parentPieceLinkId' => $this->resolveIdentifier($entry, ['parentPieceLinkId']),
|
||||||
'parentLinkId' => $this->resolveIdentifier($entry, ['parentLinkId']),
|
'parentLinkId' => $this->resolveIdentifier($entry, ['parentLinkId']),
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->entityManager->persist($link);
|
$this->entityManager->persist($link);
|
||||||
$links[$linkId] = $link;
|
$links[$linkId] = $link;
|
||||||
$keepIds[] = $linkId;
|
$keepIds[] = $linkId;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($pendingParents as $linkId => $parentIds) {
|
foreach ($pendingParents as $linkId => $parentIds) {
|
||||||
@@ -338,26 +343,33 @@ class MachineSkeletonController extends AbstractController
|
|||||||
array $productLinks,
|
array $productLinks,
|
||||||
): array {
|
): array {
|
||||||
$normalizedComponentLinks = $this->normalizeComponentLinks($componentLinks);
|
$normalizedComponentLinks = $this->normalizeComponentLinks($componentLinks);
|
||||||
$componentIndex = $this->indexNormalizedLinks($normalizedComponentLinks);
|
$componentIndex = $this->indexNormalizedLinks($normalizedComponentLinks);
|
||||||
$normalizedPieceLinks = $this->normalizePieceLinks($pieceLinks);
|
$normalizedPieceLinks = $this->normalizePieceLinks($pieceLinks);
|
||||||
|
|
||||||
// Build component hierarchy
|
// Build component hierarchy – track which IDs are children
|
||||||
foreach ($normalizedComponentLinks as &$link) {
|
$childIds = [];
|
||||||
|
foreach ($normalizedComponentLinks as $link) {
|
||||||
$parentId = $link['parentComponentLinkId'] ?? null;
|
$parentId = $link['parentComponentLinkId'] ?? null;
|
||||||
if ($parentId && isset($componentIndex[$parentId])) {
|
if ($parentId && isset($componentIndex[$parentId])) {
|
||||||
$componentIndex[$parentId]['childLinks'][] = &$link;
|
$componentIndex[$parentId]['childLinks'][] = $link;
|
||||||
|
$childIds[$link['id']] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unset($link);
|
|
||||||
|
|
||||||
// Add pieces to components recursively
|
// Add pieces to components recursively
|
||||||
$this->attachPiecesToComponents($componentIndex, $normalizedPieceLinks);
|
$this->attachPiecesToComponents($componentIndex, $normalizedPieceLinks);
|
||||||
|
|
||||||
|
// Only return root-level components (exclude children already nested)
|
||||||
|
$rootComponents = array_filter(
|
||||||
|
$componentIndex,
|
||||||
|
static fn (array $link) => !isset($childIds[$link['id']]),
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'machine' => $this->normalizeMachine($machine),
|
'machine' => $this->normalizeMachine($machine),
|
||||||
'componentLinks' => array_values($componentIndex),
|
'componentLinks' => array_values($rootComponents),
|
||||||
'pieceLinks' => $normalizedPieceLinks,
|
'pieceLinks' => $normalizedPieceLinks,
|
||||||
'productLinks' => $this->normalizeProductLinks($productLinks),
|
'productLinks' => $this->normalizeProductLinks($productLinks),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,26 +412,26 @@ class MachineSkeletonController extends AbstractController
|
|||||||
|
|
||||||
private function normalizeMachine(Machine $machine): array
|
private function normalizeMachine(Machine $machine): array
|
||||||
{
|
{
|
||||||
$site = $machine->getSite();
|
$site = $machine->getSite();
|
||||||
$typeMachine = $machine->getTypeMachine();
|
$typeMachine = $machine->getTypeMachine();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $machine->getId(),
|
'id' => $machine->getId(),
|
||||||
'name' => $machine->getName(),
|
'name' => $machine->getName(),
|
||||||
'reference' => $machine->getReference(),
|
'reference' => $machine->getReference(),
|
||||||
'prix' => $machine->getPrix(),
|
'prix' => $machine->getPrix(),
|
||||||
'siteId' => $site->getId(),
|
'siteId' => $site->getId(),
|
||||||
'site' => [
|
'site' => [
|
||||||
'id' => $site->getId(),
|
'id' => $site->getId(),
|
||||||
'name' => $site->getName(),
|
'name' => $site->getName(),
|
||||||
],
|
],
|
||||||
'typeMachineId' => $typeMachine?->getId(),
|
'typeMachineId' => $typeMachine?->getId(),
|
||||||
'typeMachine' => $typeMachine ? [
|
'typeMachine' => $typeMachine ? [
|
||||||
'id' => $typeMachine->getId(),
|
'id' => $typeMachine->getId(),
|
||||||
'name' => $typeMachine->getName(),
|
'name' => $typeMachine->getName(),
|
||||||
'category' => $typeMachine->getCategory(),
|
'category' => $typeMachine->getCategory(),
|
||||||
'description' => $typeMachine->getDescription(),
|
'description' => $typeMachine->getDescription(),
|
||||||
'customFields' => $this->normalizeCustomFields($typeMachine->getCustomFields()),
|
'customFields' => $this->normalizeCustomFields($typeMachine->getCustomFields()),
|
||||||
'componentRequirements' => $typeMachine->getComponentRequirements()
|
'componentRequirements' => $typeMachine->getComponentRequirements()
|
||||||
->map(fn (TypeMachineComponentRequirement $req) => $this->normalizeComponentRequirement($req))
|
->map(fn (TypeMachineComponentRequirement $req) => $this->normalizeComponentRequirement($req))
|
||||||
->toArray(),
|
->toArray(),
|
||||||
@@ -430,8 +442,8 @@ class MachineSkeletonController extends AbstractController
|
|||||||
->map(fn (TypeMachineProductRequirement $req) => $this->normalizeProductRequirement($req))
|
->map(fn (TypeMachineProductRequirement $req) => $this->normalizeProductRequirement($req))
|
||||||
->toArray(),
|
->toArray(),
|
||||||
] : null,
|
] : null,
|
||||||
'constructeurs' => $this->normalizeConstructeurs($machine->getConstructeurs()),
|
'constructeurs' => $this->normalizeConstructeurs($machine->getConstructeurs()),
|
||||||
'documents' => null,
|
'documents' => null,
|
||||||
'customFieldValues' => null,
|
'customFieldValues' => null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -444,13 +456,13 @@ class MachineSkeletonController extends AbstractController
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'id' => $customField->getId(),
|
'id' => $customField->getId(),
|
||||||
'name' => $customField->getName(),
|
'name' => $customField->getName(),
|
||||||
'type' => $customField->getType(),
|
'type' => $customField->getType(),
|
||||||
'required' => $customField->isRequired(),
|
'required' => $customField->isRequired(),
|
||||||
'options' => $customField->getOptions(),
|
'options' => $customField->getOptions(),
|
||||||
'defaultValue' => $customField->getDefaultValue(),
|
'defaultValue' => $customField->getDefaultValue(),
|
||||||
'orderIndex' => $customField->getOrderIndex(),
|
'orderIndex' => $customField->getOrderIndex(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,26 +472,26 @@ class MachineSkeletonController extends AbstractController
|
|||||||
private function normalizeComponentLinks(array $links): array
|
private function normalizeComponentLinks(array $links): array
|
||||||
{
|
{
|
||||||
return array_map(function (MachineComponentLink $link): array {
|
return array_map(function (MachineComponentLink $link): array {
|
||||||
$composant = $link->getComposant();
|
$composant = $link->getComposant();
|
||||||
$requirement = $link->getTypeMachineComponentRequirement();
|
$requirement = $link->getTypeMachineComponentRequirement();
|
||||||
$parentLink = $link->getParentLink();
|
$parentLink = $link->getParentLink();
|
||||||
$parentRequirementId = $parentLink?->getTypeMachineComponentRequirement()?->getId();
|
$parentRequirementId = $parentLink?->getTypeMachineComponentRequirement()?->getId();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $link->getId(),
|
'id' => $link->getId(),
|
||||||
'linkId' => $link->getId(),
|
'linkId' => $link->getId(),
|
||||||
'machineId' => $link->getMachine()->getId(),
|
'machineId' => $link->getMachine()->getId(),
|
||||||
'composantId' => $composant->getId(),
|
'composantId' => $composant->getId(),
|
||||||
'composant' => $this->normalizeComposant($composant),
|
'composant' => $this->normalizeComposant($composant),
|
||||||
'typeMachineComponentRequirementId' => $requirement?->getId(),
|
'typeMachineComponentRequirementId' => $requirement?->getId(),
|
||||||
'typeMachineComponentRequirement' => $requirement ? $this->normalizeComponentRequirement($requirement) : null,
|
'typeMachineComponentRequirement' => $requirement ? $this->normalizeComponentRequirement($requirement) : null,
|
||||||
'parentLinkId' => $parentLink?->getId(),
|
'parentLinkId' => $parentLink?->getId(),
|
||||||
'parentComponentLinkId' => $parentLink?->getId(),
|
'parentComponentLinkId' => $parentLink?->getId(),
|
||||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||||
'parentMachineComponentRequirementId' => $parentRequirementId,
|
'parentMachineComponentRequirementId' => $parentRequirementId,
|
||||||
'overrides' => $this->normalizeOverrides($link),
|
'overrides' => $this->normalizeOverrides($link),
|
||||||
'childLinks' => [],
|
'childLinks' => [],
|
||||||
'pieceLinks' => [],
|
'pieceLinks' => [],
|
||||||
];
|
];
|
||||||
}, $links);
|
}, $links);
|
||||||
}
|
}
|
||||||
@@ -487,24 +499,24 @@ class MachineSkeletonController extends AbstractController
|
|||||||
private function normalizePieceLinks(array $links): array
|
private function normalizePieceLinks(array $links): array
|
||||||
{
|
{
|
||||||
return array_map(function (MachinePieceLink $link): array {
|
return array_map(function (MachinePieceLink $link): array {
|
||||||
$piece = $link->getPiece();
|
$piece = $link->getPiece();
|
||||||
$requirement = $link->getTypeMachinePieceRequirement();
|
$requirement = $link->getTypeMachinePieceRequirement();
|
||||||
$parentLink = $link->getParentLink();
|
$parentLink = $link->getParentLink();
|
||||||
$parentRequirementId = $parentLink?->getTypeMachineComponentRequirement()?->getId();
|
$parentRequirementId = $parentLink?->getTypeMachineComponentRequirement()?->getId();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $link->getId(),
|
'id' => $link->getId(),
|
||||||
'linkId' => $link->getId(),
|
'linkId' => $link->getId(),
|
||||||
'machineId' => $link->getMachine()->getId(),
|
'machineId' => $link->getMachine()->getId(),
|
||||||
'pieceId' => $piece->getId(),
|
'pieceId' => $piece->getId(),
|
||||||
'piece' => $this->normalizePiece($piece),
|
'piece' => $this->normalizePiece($piece),
|
||||||
'typeMachinePieceRequirementId' => $requirement?->getId(),
|
'typeMachinePieceRequirementId' => $requirement?->getId(),
|
||||||
'typeMachinePieceRequirement' => $requirement ? $this->normalizePieceRequirement($requirement) : null,
|
'typeMachinePieceRequirement' => $requirement ? $this->normalizePieceRequirement($requirement) : null,
|
||||||
'parentLinkId' => $parentLink?->getId(),
|
'parentLinkId' => $parentLink?->getId(),
|
||||||
'parentComponentLinkId' => $parentLink?->getId(),
|
'parentComponentLinkId' => $parentLink?->getId(),
|
||||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||||
'parentMachineComponentRequirementId' => $parentRequirementId,
|
'parentMachineComponentRequirementId' => $parentRequirementId,
|
||||||
'overrides' => $this->normalizeOverrides($link),
|
'overrides' => $this->normalizeOverrides($link),
|
||||||
];
|
];
|
||||||
}, $links);
|
}, $links);
|
||||||
}
|
}
|
||||||
@@ -512,20 +524,20 @@ class MachineSkeletonController extends AbstractController
|
|||||||
private function normalizeProductLinks(array $links): array
|
private function normalizeProductLinks(array $links): array
|
||||||
{
|
{
|
||||||
return array_map(function (MachineProductLink $link): array {
|
return array_map(function (MachineProductLink $link): array {
|
||||||
$product = $link->getProduct();
|
$product = $link->getProduct();
|
||||||
$requirement = $link->getTypeMachineProductRequirement();
|
$requirement = $link->getTypeMachineProductRequirement();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $link->getId(),
|
'id' => $link->getId(),
|
||||||
'linkId' => $link->getId(),
|
'linkId' => $link->getId(),
|
||||||
'machineId' => $link->getMachine()->getId(),
|
'machineId' => $link->getMachine()->getId(),
|
||||||
'productId' => $product->getId(),
|
'productId' => $product->getId(),
|
||||||
'product' => $this->normalizeProduct($product),
|
'product' => $this->normalizeProduct($product),
|
||||||
'typeMachineProductRequirementId' => $requirement?->getId(),
|
'typeMachineProductRequirementId' => $requirement?->getId(),
|
||||||
'typeMachineProductRequirement' => $requirement ? $this->normalizeProductRequirement($requirement) : null,
|
'typeMachineProductRequirement' => $requirement ? $this->normalizeProductRequirement($requirement) : null,
|
||||||
'parentLinkId' => $link->getParentLink()?->getId(),
|
'parentLinkId' => $link->getParentLink()?->getId(),
|
||||||
'parentComponentLinkId' => $link->getParentComponentLink()?->getId(),
|
'parentComponentLinkId' => $link->getParentComponentLink()?->getId(),
|
||||||
'parentPieceLinkId' => $link->getParentPieceLink()?->getId(),
|
'parentPieceLinkId' => $link->getParentPieceLink()?->getId(),
|
||||||
];
|
];
|
||||||
}, $links);
|
}, $links);
|
||||||
}
|
}
|
||||||
@@ -533,49 +545,49 @@ class MachineSkeletonController extends AbstractController
|
|||||||
private function normalizeComposant(Composant $composant): array
|
private function normalizeComposant(Composant $composant): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $composant->getId(),
|
'id' => $composant->getId(),
|
||||||
'name' => $composant->getName(),
|
'name' => $composant->getName(),
|
||||||
'reference' => $composant->getReference(),
|
'reference' => $composant->getReference(),
|
||||||
'prix' => $composant->getPrix(),
|
'prix' => $composant->getPrix(),
|
||||||
'typeComposantId' => $composant->getTypeComposant()?->getId(),
|
'typeComposantId' => $composant->getTypeComposant()?->getId(),
|
||||||
'typeComposant' => $this->normalizeModelType($composant->getTypeComposant()),
|
'typeComposant' => $this->normalizeModelType($composant->getTypeComposant()),
|
||||||
'productId' => $composant->getProduct()?->getId(),
|
'productId' => $composant->getProduct()?->getId(),
|
||||||
'product' => $composant->getProduct() ? $this->normalizeProduct($composant->getProduct()) : null,
|
'product' => $composant->getProduct() ? $this->normalizeProduct($composant->getProduct()) : null,
|
||||||
'constructeurs' => $this->normalizeConstructeurs($composant->getConstructeurs()),
|
'constructeurs' => $this->normalizeConstructeurs($composant->getConstructeurs()),
|
||||||
'documents' => [],
|
'documents' => [],
|
||||||
'customFields' => [],
|
'customFields' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizePiece(Piece $piece): array
|
private function normalizePiece(Piece $piece): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $piece->getId(),
|
'id' => $piece->getId(),
|
||||||
'name' => $piece->getName(),
|
'name' => $piece->getName(),
|
||||||
'reference' => $piece->getReference(),
|
'reference' => $piece->getReference(),
|
||||||
'prix' => $piece->getPrix(),
|
'prix' => $piece->getPrix(),
|
||||||
'typePieceId' => $piece->getTypePiece()?->getId(),
|
'typePieceId' => $piece->getTypePiece()?->getId(),
|
||||||
'typePiece' => $this->normalizeModelType($piece->getTypePiece()),
|
'typePiece' => $this->normalizeModelType($piece->getTypePiece()),
|
||||||
'productId' => $piece->getProduct()?->getId(),
|
'productId' => $piece->getProduct()?->getId(),
|
||||||
'product' => $piece->getProduct() ? $this->normalizeProduct($piece->getProduct()) : null,
|
'product' => $piece->getProduct() ? $this->normalizeProduct($piece->getProduct()) : null,
|
||||||
'constructeurs' => $this->normalizeConstructeurs($piece->getConstructeurs()),
|
'constructeurs' => $this->normalizeConstructeurs($piece->getConstructeurs()),
|
||||||
'documents' => [],
|
'documents' => [],
|
||||||
'customFields' => [],
|
'customFields' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeProduct(Product $product): array
|
private function normalizeProduct(Product $product): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $product->getId(),
|
'id' => $product->getId(),
|
||||||
'name' => $product->getName(),
|
'name' => $product->getName(),
|
||||||
'reference' => $product->getReference(),
|
'reference' => $product->getReference(),
|
||||||
'supplierPrice' => $product->getSupplierPrice(),
|
'supplierPrice' => $product->getSupplierPrice(),
|
||||||
'typeProductId' => $product->getTypeProduct()?->getId(),
|
'typeProductId' => $product->getTypeProduct()?->getId(),
|
||||||
'typeProduct' => $this->normalizeModelType($product->getTypeProduct()),
|
'typeProduct' => $this->normalizeModelType($product->getTypeProduct()),
|
||||||
'constructeurs' => $this->normalizeConstructeurs($product->getConstructeurs()),
|
'constructeurs' => $this->normalizeConstructeurs($product->getConstructeurs()),
|
||||||
'documents' => [],
|
'documents' => [],
|
||||||
'customFields' => [],
|
'customFields' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,9 +598,9 @@ class MachineSkeletonController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $type->getId(),
|
'id' => $type->getId(),
|
||||||
'name' => $type->getName(),
|
'name' => $type->getName(),
|
||||||
'code' => $type->getCode(),
|
'code' => $type->getCode(),
|
||||||
'category' => $type->getCategory()->value,
|
'category' => $type->getCategory()->value,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -596,39 +608,39 @@ class MachineSkeletonController extends AbstractController
|
|||||||
private function normalizeComponentRequirement(TypeMachineComponentRequirement $requirement): array
|
private function normalizeComponentRequirement(TypeMachineComponentRequirement $requirement): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $requirement->getId(),
|
'id' => $requirement->getId(),
|
||||||
'label' => $requirement->getLabel(),
|
'label' => $requirement->getLabel(),
|
||||||
'minCount' => $requirement->getMinCount(),
|
'minCount' => $requirement->getMinCount(),
|
||||||
'maxCount' => $requirement->getMaxCount(),
|
'maxCount' => $requirement->getMaxCount(),
|
||||||
'required' => $requirement->isRequired(),
|
'required' => $requirement->isRequired(),
|
||||||
'typeComposantId' => $requirement->getTypeComposant()->getId(),
|
'typeComposantId' => $requirement->getTypeComposant()->getId(),
|
||||||
'typeComposant' => $this->normalizeModelType($requirement->getTypeComposant()),
|
'typeComposant' => $this->normalizeModelType($requirement->getTypeComposant()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizePieceRequirement(TypeMachinePieceRequirement $requirement): array
|
private function normalizePieceRequirement(TypeMachinePieceRequirement $requirement): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $requirement->getId(),
|
'id' => $requirement->getId(),
|
||||||
'label' => $requirement->getLabel(),
|
'label' => $requirement->getLabel(),
|
||||||
'minCount' => $requirement->getMinCount(),
|
'minCount' => $requirement->getMinCount(),
|
||||||
'maxCount' => $requirement->getMaxCount(),
|
'maxCount' => $requirement->getMaxCount(),
|
||||||
'required' => $requirement->isRequired(),
|
'required' => $requirement->isRequired(),
|
||||||
'typePieceId' => $requirement->getTypePiece()->getId(),
|
'typePieceId' => $requirement->getTypePiece()->getId(),
|
||||||
'typePiece' => $this->normalizeModelType($requirement->getTypePiece()),
|
'typePiece' => $this->normalizeModelType($requirement->getTypePiece()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeProductRequirement(TypeMachineProductRequirement $requirement): array
|
private function normalizeProductRequirement(TypeMachineProductRequirement $requirement): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $requirement->getId(),
|
'id' => $requirement->getId(),
|
||||||
'label' => $requirement->getLabel(),
|
'label' => $requirement->getLabel(),
|
||||||
'minCount' => $requirement->getMinCount(),
|
'minCount' => $requirement->getMinCount(),
|
||||||
'maxCount' => $requirement->getMaxCount(),
|
'maxCount' => $requirement->getMaxCount(),
|
||||||
'required' => $requirement->isRequired(),
|
'required' => $requirement->isRequired(),
|
||||||
'typeProductId' => $requirement->getTypeProduct()->getId(),
|
'typeProductId' => $requirement->getTypeProduct()->getId(),
|
||||||
'typeProduct' => $this->normalizeModelType($requirement->getTypeProduct()),
|
'typeProduct' => $this->normalizeModelType($requirement->getTypeProduct()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,8 +649,8 @@ class MachineSkeletonController extends AbstractController
|
|||||||
$items = [];
|
$items = [];
|
||||||
foreach ($constructeurs as $constructeur) {
|
foreach ($constructeurs as $constructeur) {
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'id' => $constructeur->getId(),
|
'id' => $constructeur->getId(),
|
||||||
'name' => $constructeur->getName(),
|
'name' => $constructeur->getName(),
|
||||||
'email' => $constructeur->getEmail(),
|
'email' => $constructeur->getEmail(),
|
||||||
'phone' => $constructeur->getPhone(),
|
'phone' => $constructeur->getPhone(),
|
||||||
];
|
];
|
||||||
@@ -649,18 +661,18 @@ class MachineSkeletonController extends AbstractController
|
|||||||
|
|
||||||
private function normalizeOverrides(object $link): ?array
|
private function normalizeOverrides(object $link): ?array
|
||||||
{
|
{
|
||||||
$name = method_exists($link, 'getNameOverride') ? $link->getNameOverride() : null;
|
$name = method_exists($link, 'getNameOverride') ? $link->getNameOverride() : null;
|
||||||
$reference = method_exists($link, 'getReferenceOverride') ? $link->getReferenceOverride() : null;
|
$reference = method_exists($link, 'getReferenceOverride') ? $link->getReferenceOverride() : null;
|
||||||
$prix = method_exists($link, 'getPrixOverride') ? $link->getPrixOverride() : null;
|
$prix = method_exists($link, 'getPrixOverride') ? $link->getPrixOverride() : null;
|
||||||
|
|
||||||
if ($name === null && $reference === null && $prix === null) {
|
if (null === $name && null === $reference && null === $prix) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'reference' => $reference,
|
'reference' => $reference,
|
||||||
'prix' => $prix,
|
'prix' => $prix,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,12 +695,12 @@ class MachineSkeletonController extends AbstractController
|
|||||||
|
|
||||||
private function stringOrNull(mixed $value): ?string
|
private function stringOrNull(mixed $value): ?string
|
||||||
{
|
{
|
||||||
if ($value === null) {
|
if (null === $value) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$string = trim((string) $value);
|
$string = trim((string) $value);
|
||||||
|
|
||||||
return $string === '' ? null : $string;
|
return '' === $string ? null : $string;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveIdentifier(array $entry, array $keys): ?string
|
private function resolveIdentifier(array $entry, array $keys): ?string
|
||||||
@@ -698,9 +710,10 @@ class MachineSkeletonController extends AbstractController
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$value = $entry[$key];
|
$value = $entry[$key];
|
||||||
if ($value === null || $value === '') {
|
if (null === $value || '' === $value) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (string) $value;
|
return (string) $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,6 +722,7 @@ class MachineSkeletonController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<array-key, object> $links
|
* @param array<array-key, object> $links
|
||||||
|
*
|
||||||
* @return array<string, object>
|
* @return array<string, object>
|
||||||
*/
|
*/
|
||||||
private function indexLinksById(array $links): array
|
private function indexLinksById(array $links): array
|
||||||
@@ -751,6 +765,6 @@ class MachineSkeletonController extends AbstractController
|
|||||||
|
|
||||||
private function generateCuid(): string
|
private function generateCuid(): string
|
||||||
{
|
{
|
||||||
return 'cl' . bin2hex(random_bytes(12));
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/Controller/ModelTypeConversionController.php
Normal file
60
src/Controller/ModelTypeConversionController.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\ModelTypeRepository;
|
||||||
|
use App\Service\ModelTypeCategoryConversionService;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class ModelTypeConversionController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ModelTypeRepository $modelTypes,
|
||||||
|
private readonly ModelTypeCategoryConversionService $conversionService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/model_types/{id}/conversion-check', name: 'api_model_type_conversion_check', methods: ['GET'])]
|
||||||
|
public function check(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$modelType = $this->modelTypes->find($id);
|
||||||
|
|
||||||
|
if (!$modelType) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Catégorie introuvable.'],
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse($this->conversionService->checkConversion($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/model_types/{id}/convert', name: 'api_model_type_convert', methods: ['POST'])]
|
||||||
|
public function convert(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
|
$modelType = $this->modelTypes->find($id);
|
||||||
|
|
||||||
|
if (!$modelType) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Catégorie introuvable.'],
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->conversionService->convert($id);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return new JsonResponse($result, Response::HTTP_CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,22 +7,25 @@ namespace App\Controller;
|
|||||||
use App\Repository\AuditLogRepository;
|
use App\Repository\AuditLogRepository;
|
||||||
use App\Repository\PieceRepository;
|
use App\Repository\PieceRepository;
|
||||||
use App\Repository\ProfileRepository;
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
final class PieceHistoryController
|
final class PieceHistoryController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PieceRepository $pieces,
|
private readonly PieceRepository $pieces,
|
||||||
private readonly AuditLogRepository $auditLogs,
|
private readonly AuditLogRepository $auditLogs,
|
||||||
private readonly ProfileRepository $profiles,
|
private readonly ProfileRepository $profiles,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/api/pieces/{id}/history', name: 'api_piece_history', methods: ['GET'])]
|
#[Route('/api/pieces/{id}/history', name: 'api_piece_history', methods: ['GET'])]
|
||||||
public function __invoke(string $id): JsonResponse
|
public function __invoke(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
$piece = $this->pieces->find($id);
|
$piece = $this->pieces->find($id);
|
||||||
if (!$piece) {
|
if (!$piece) {
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
@@ -39,11 +42,11 @@ final class PieceHistoryController
|
|||||||
))));
|
))));
|
||||||
|
|
||||||
$actorMap = [];
|
$actorMap = [];
|
||||||
if ($actorIds !== []) {
|
if ([] !== $actorIds) {
|
||||||
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||||
foreach ($profiles as $profile) {
|
foreach ($profiles as $profile) {
|
||||||
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
if ($label === '') {
|
if ('' === $label) {
|
||||||
$label = $profile->getEmail() ?? $profile->getId();
|
$label = $profile->getEmail() ?? $profile->getId();
|
||||||
}
|
}
|
||||||
$actorMap[$profile->getId()] = $label;
|
$actorMap[$profile->getId()] = $label;
|
||||||
@@ -55,16 +58,16 @@ final class PieceHistoryController
|
|||||||
$actorId = $log->getActorProfileId();
|
$actorId = $log->getActorProfileId();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $log->getId(),
|
'id' => $log->getId(),
|
||||||
'action' => $log->getAction(),
|
'action' => $log->getAction(),
|
||||||
'createdAt' => $log->getCreatedAt()->format(\DateTimeInterface::ATOM),
|
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
'actor' => $actorId
|
'actor' => $actorId
|
||||||
? [
|
? [
|
||||||
'id' => $actorId,
|
'id' => $actorId,
|
||||||
'label' => $actorMap[$actorId] ?? $actorId,
|
'label' => $actorMap[$actorId] ?? $actorId,
|
||||||
]
|
]
|
||||||
: null,
|
: null,
|
||||||
'diff' => $log->getDiff(),
|
'diff' => $log->getDiff(),
|
||||||
'snapshot' => $log->getSnapshot(),
|
'snapshot' => $log->getSnapshot(),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@@ -77,4 +80,3 @@ final class PieceHistoryController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,22 +7,25 @@ namespace App\Controller;
|
|||||||
use App\Repository\AuditLogRepository;
|
use App\Repository\AuditLogRepository;
|
||||||
use App\Repository\ProductRepository;
|
use App\Repository\ProductRepository;
|
||||||
use App\Repository\ProfileRepository;
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
final class ProductHistoryController
|
final class ProductHistoryController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ProductRepository $products,
|
private readonly ProductRepository $products,
|
||||||
private readonly AuditLogRepository $auditLogs,
|
private readonly AuditLogRepository $auditLogs,
|
||||||
private readonly ProfileRepository $profiles,
|
private readonly ProfileRepository $profiles,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/api/products/{id}/history', name: 'api_product_history', methods: ['GET'])]
|
#[Route('/api/products/{id}/history', name: 'api_product_history', methods: ['GET'])]
|
||||||
public function __invoke(string $id): JsonResponse
|
public function __invoke(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
$product = $this->products->find($id);
|
$product = $this->products->find($id);
|
||||||
if (!$product) {
|
if (!$product) {
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
@@ -39,11 +42,11 @@ final class ProductHistoryController
|
|||||||
))));
|
))));
|
||||||
|
|
||||||
$actorMap = [];
|
$actorMap = [];
|
||||||
if ($actorIds !== []) {
|
if ([] !== $actorIds) {
|
||||||
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||||
foreach ($profiles as $profile) {
|
foreach ($profiles as $profile) {
|
||||||
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
if ($label === '') {
|
if ('' === $label) {
|
||||||
$label = $profile->getEmail() ?? $profile->getId();
|
$label = $profile->getEmail() ?? $profile->getId();
|
||||||
}
|
}
|
||||||
$actorMap[$profile->getId()] = $label;
|
$actorMap[$profile->getId()] = $label;
|
||||||
@@ -55,16 +58,16 @@ final class ProductHistoryController
|
|||||||
$actorId = $log->getActorProfileId();
|
$actorId = $log->getActorProfileId();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $log->getId(),
|
'id' => $log->getId(),
|
||||||
'action' => $log->getAction(),
|
'action' => $log->getAction(),
|
||||||
'createdAt' => $log->getCreatedAt()->format(\DateTimeInterface::ATOM),
|
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
'actor' => $actorId
|
'actor' => $actorId
|
||||||
? [
|
? [
|
||||||
'id' => $actorId,
|
'id' => $actorId,
|
||||||
'label' => $actorMap[$actorId] ?? $actorId,
|
'label' => $actorMap[$actorId] ?? $actorId,
|
||||||
]
|
]
|
||||||
: null,
|
: null,
|
||||||
'diff' => $log->getDiff(),
|
'diff' => $log->getDiff(),
|
||||||
'snapshot' => $log->getSnapshot(),
|
'snapshot' => $log->getSnapshot(),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@@ -77,4 +80,3 @@ final class ProductHistoryController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ use App\Repository\ProfileRepository;
|
|||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
final class SessionProfileController
|
final class SessionProfileController
|
||||||
{
|
{
|
||||||
public function __construct(private readonly ProfileRepository $profiles)
|
public function __construct(
|
||||||
{
|
private readonly ProfileRepository $profiles,
|
||||||
}
|
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||||
|
) {}
|
||||||
|
|
||||||
#[Route('/api/session/profile', name: 'api_session_profile_get', methods: ['GET'])]
|
#[Route('/api/session/profile', name: 'api_session_profile_get', methods: ['GET'])]
|
||||||
public function getActiveProfile(Request $request): JsonResponse
|
public function getActiveProfile(Request $request): JsonResponse
|
||||||
@@ -32,16 +34,17 @@ final class SessionProfileController
|
|||||||
$profile = $this->profiles->find($profileId);
|
$profile = $this->profiles->find($profileId);
|
||||||
if (!$profile || !$profile->isActive()) {
|
if (!$profile || !$profile->isActive()) {
|
||||||
$session->remove('profileId');
|
$session->remove('profileId');
|
||||||
|
|
||||||
return new JsonResponse(['message' => 'Profil introuvable ou inactif.'], JsonResponse::HTTP_UNAUTHORIZED);
|
return new JsonResponse(['message' => 'Profil introuvable ou inactif.'], JsonResponse::HTTP_UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'id' => $profile->getId(),
|
'id' => $profile->getId(),
|
||||||
'firstName' => $profile->getFirstName(),
|
'firstName' => $profile->getFirstName(),
|
||||||
'lastName' => $profile->getLastName(),
|
'lastName' => $profile->getLastName(),
|
||||||
'email' => $profile->getEmail(),
|
'email' => $profile->getEmail(),
|
||||||
'isActive' => $profile->isActive(),
|
'isActive' => $profile->isActive(),
|
||||||
'roles' => $profile->getRoles(),
|
'roles' => $profile->getRoles(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +56,7 @@ final class SessionProfileController
|
|||||||
return new JsonResponse(['message' => 'Session indisponible.'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
|
return new JsonResponse(['message' => 'Session indisponible.'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload = $request->toArray();
|
$payload = $request->toArray();
|
||||||
$profileId = $payload['profileId'] ?? null;
|
$profileId = $payload['profileId'] ?? null;
|
||||||
|
|
||||||
if (!$profileId) {
|
if (!$profileId) {
|
||||||
@@ -65,15 +68,32 @@ final class SessionProfileController
|
|||||||
return new JsonResponse(['message' => 'Profil introuvable ou inactif.'], JsonResponse::HTTP_UNAUTHORIZED);
|
return new JsonResponse(['message' => 'Profil introuvable ou inactif.'], JsonResponse::HTTP_UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$password = $payload['password'] ?? '';
|
||||||
|
if ('' === $password) {
|
||||||
|
return new JsonResponse(['message' => 'Mot de passe requis.'], JsonResponse::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$profile->getPassword()) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Ce profil n\'a pas de mot de passe. Contactez un administrateur.'],
|
||||||
|
JsonResponse::HTTP_FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->passwordHasher->isPasswordValid($profile, $password)) {
|
||||||
|
return new JsonResponse(['message' => 'Mot de passe incorrect.'], JsonResponse::HTTP_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
$session->set('profileId', $profile->getId());
|
$session->set('profileId', $profile->getId());
|
||||||
|
$session->set('profileRoles', $profile->getRoles());
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'id' => $profile->getId(),
|
'id' => $profile->getId(),
|
||||||
'firstName' => $profile->getFirstName(),
|
'firstName' => $profile->getFirstName(),
|
||||||
'lastName' => $profile->getLastName(),
|
'lastName' => $profile->getLastName(),
|
||||||
'email' => $profile->getEmail(),
|
'email' => $profile->getEmail(),
|
||||||
'isActive' => $profile->isActive(),
|
'isActive' => $profile->isActive(),
|
||||||
'roles' => $profile->getRoles(),
|
'roles' => $profile->getRoles(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,20 +4,15 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\Profile;
|
|
||||||
use App\Repository\ProfileRepository;
|
use App\Repository\ProfileRepository;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
final class SessionProfilesController
|
final class SessionProfilesController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ProfileRepository $profiles,
|
private readonly ProfileRepository $profiles,
|
||||||
private readonly EntityManagerInterface $entityManager
|
) {}
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/api/session/profiles', name: 'api_session_profiles_list', methods: ['GET'])]
|
#[Route('/api/session/profiles', name: 'api_session_profiles_list', methods: ['GET'])]
|
||||||
public function list(): JsonResponse
|
public function list(): JsonResponse
|
||||||
@@ -27,54 +22,16 @@ final class SessionProfilesController
|
|||||||
->setParameter('active', true)
|
->setParameter('active', true)
|
||||||
->orderBy('p.firstName', 'ASC')
|
->orderBy('p.firstName', 'ASC')
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->getResult();
|
->getResult()
|
||||||
|
;
|
||||||
|
|
||||||
return new JsonResponse(array_map([$this, 'serializeProfile'], $items));
|
return new JsonResponse(array_map(static function ($profile): array {
|
||||||
}
|
return [
|
||||||
|
'id' => $profile->getId(),
|
||||||
#[Route('/api/session/profiles', name: 'api_session_profiles_create', methods: ['POST'])]
|
'firstName' => $profile->getFirstName(),
|
||||||
public function create(Request $request): JsonResponse
|
'lastName' => $profile->getLastName(),
|
||||||
{
|
'hasPassword' => null !== $profile->getPassword() && '' !== $profile->getPassword(),
|
||||||
$payload = $request->toArray();
|
];
|
||||||
$firstName = trim((string) ($payload['firstName'] ?? ''));
|
}, $items));
|
||||||
$lastName = trim((string) ($payload['lastName'] ?? ''));
|
|
||||||
|
|
||||||
if ($firstName === '' || $lastName === '') {
|
|
||||||
return new JsonResponse(['message' => 'firstName et lastName sont requis.'], JsonResponse::HTTP_BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
$profile = new Profile();
|
|
||||||
$profile->setFirstName($firstName);
|
|
||||||
$profile->setLastName($lastName);
|
|
||||||
$profile->setIsActive(true);
|
|
||||||
|
|
||||||
$this->entityManager->persist($profile);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
return new JsonResponse($this->serializeProfile($profile), JsonResponse::HTTP_CREATED);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/api/session/profiles/{id}', name: 'api_session_profiles_delete', methods: ['DELETE'])]
|
|
||||||
public function delete(string $id): JsonResponse
|
|
||||||
{
|
|
||||||
$profile = $this->profiles->find($id);
|
|
||||||
if (!$profile) {
|
|
||||||
return new JsonResponse(['message' => 'Profil introuvable.'], JsonResponse::HTTP_NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
$profile->setIsActive(false);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
return new JsonResponse(['success' => true]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function serializeProfile(Profile $profile): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => $profile->getId(),
|
|
||||||
'firstName' => $profile->getFirstName(),
|
|
||||||
'lastName' => $profile->getLastName(),
|
|
||||||
'isActive' => $profile->isActive(),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ final class AlwaysQuoteStrategy implements QuoteStrategy
|
|||||||
{
|
{
|
||||||
$tableName = $platform->quoteSingleIdentifier($class->table['name']);
|
$tableName = $platform->quoteSingleIdentifier($class->table['name']);
|
||||||
|
|
||||||
if (! empty($class->table['schema'])) {
|
if (!empty($class->table['schema'])) {
|
||||||
return $platform->quoteSingleIdentifier($class->table['schema']) . '.' . $tableName;
|
return $platform->quoteSingleIdentifier($class->table['schema']).'.'.$tableName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $tableName;
|
return $tableName;
|
||||||
@@ -56,10 +56,10 @@ final class AlwaysQuoteStrategy implements QuoteStrategy
|
|||||||
$schema = '';
|
$schema = '';
|
||||||
|
|
||||||
if (isset($association->joinTable->schema)) {
|
if (isset($association->joinTable->schema)) {
|
||||||
$schema = $platform->quoteSingleIdentifier($association->joinTable->schema) . '.';
|
$schema = $platform->quoteSingleIdentifier($association->joinTable->schema).'.';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $schema . $platform->quoteSingleIdentifier($association->joinTable->name);
|
return $schema.$platform->quoteSingleIdentifier($association->joinTable->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string
|
public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string
|
||||||
@@ -82,12 +82,13 @@ final class AlwaysQuoteStrategy implements QuoteStrategy
|
|||||||
foreach ($class->identifier as $fieldName) {
|
foreach ($class->identifier as $fieldName) {
|
||||||
if (isset($class->fieldMappings[$fieldName])) {
|
if (isset($class->fieldMappings[$fieldName])) {
|
||||||
$quotedColumnNames[] = $this->getColumnName($fieldName, $class, $platform);
|
$quotedColumnNames[] = $this->getColumnName($fieldName, $class, $platform);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$assoc = $class->associationMappings[$fieldName];
|
$assoc = $class->associationMappings[$fieldName];
|
||||||
assert($assoc->isToOneOwningSide());
|
assert($assoc->isToOneOwningSide());
|
||||||
$joinColumns = $assoc->joinColumns;
|
$joinColumns = $assoc->joinColumns;
|
||||||
$assocQuotedColumnNames = array_map(
|
$assocQuotedColumnNames = array_map(
|
||||||
static fn (JoinColumnMapping $joinColumn) => $platform->quoteSingleIdentifier($joinColumn->name),
|
static fn (JoinColumnMapping $joinColumn) => $platform->quoteSingleIdentifier($joinColumn->name),
|
||||||
$joinColumns,
|
$joinColumns,
|
||||||
@@ -103,8 +104,8 @@ final class AlwaysQuoteStrategy implements QuoteStrategy
|
|||||||
string $columnName,
|
string $columnName,
|
||||||
int $counter,
|
int $counter,
|
||||||
AbstractPlatform $platform,
|
AbstractPlatform $platform,
|
||||||
ClassMetadata|null $class = null,
|
?ClassMetadata $class = null,
|
||||||
): string {
|
): string {
|
||||||
return $this->getSQLResultCasing($platform, $columnName . '_' . $counter);
|
return $this->getSQLResultCasing($platform, $columnName.'_'.$counter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ class AuditLog
|
|||||||
?array $snapshot = null,
|
?array $snapshot = null,
|
||||||
?string $actorProfileId = null,
|
?string $actorProfileId = null,
|
||||||
) {
|
) {
|
||||||
$this->entityType = $entityType;
|
$this->entityType = $entityType;
|
||||||
$this->entityId = $entityId;
|
$this->entityId = $entityId;
|
||||||
$this->action = $action;
|
$this->action = $action;
|
||||||
$this->diff = $diff;
|
$this->diff = $diff;
|
||||||
$this->snapshot = $snapshot;
|
$this->snapshot = $snapshot;
|
||||||
$this->actorProfileId = $actorProfileId;
|
$this->actorProfileId = $actorProfileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ class AuditLog
|
|||||||
$this->createdAt = new DateTimeImmutable();
|
$this->createdAt = new DateTimeImmutable();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->id === null) {
|
if (null === $this->id) {
|
||||||
$this->id = $this->generateCuid();
|
$this->id = $this->generateCuid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
235
src/Entity/Comment.php
Normal file
235
src/Entity/Comment.php
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'comments')]
|
||||||
|
#[ORM\Index(columns: ['entity_type', 'entity_id', 'status'], name: 'idx_comment_entity_status')]
|
||||||
|
#[ORM\HasLifecycleCallbacks]
|
||||||
|
#[ApiFilter(SearchFilter::class, properties: ['entityType' => 'exact', 'entityId' => 'exact', 'status' => 'exact'])]
|
||||||
|
#[ApiFilter(OrderFilter::class, properties: ['createdAt'])]
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
],
|
||||||
|
order: ['createdAt' => 'DESC'],
|
||||||
|
paginationClientItemsPerPage: true,
|
||||||
|
paginationMaximumItemsPerPage: 200
|
||||||
|
)]
|
||||||
|
class Comment
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT)]
|
||||||
|
private string $content;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 50, name: 'entity_type')]
|
||||||
|
private string $entityType;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36, name: 'entity_id')]
|
||||||
|
private string $entityId;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'entity_name')]
|
||||||
|
private ?string $entityName = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36, name: 'author_id')]
|
||||||
|
private string $authorId;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, name: 'author_name')]
|
||||||
|
private string $authorName;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 20)]
|
||||||
|
private string $status = 'open';
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36, nullable: true, name: 'resolved_by_id')]
|
||||||
|
private ?string $resolvedById = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'resolved_by_name')]
|
||||||
|
private ?string $resolvedByName = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true, name: 'resolved_at')]
|
||||||
|
private ?DateTimeImmutable $resolvedAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'created_at')]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updated_at')]
|
||||||
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
|
#[ORM\PrePersist]
|
||||||
|
public function setCreatedAtValue(): void
|
||||||
|
{
|
||||||
|
$now = new DateTimeImmutable();
|
||||||
|
$this->createdAt = $now;
|
||||||
|
$this->updatedAt = $now;
|
||||||
|
|
||||||
|
if (null === $this->id) {
|
||||||
|
$this->id = $this->generateCuid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ORM\PreUpdate]
|
||||||
|
public function setUpdatedAtValue(): void
|
||||||
|
{
|
||||||
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?string
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContent(): string
|
||||||
|
{
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContent(string $content): static
|
||||||
|
{
|
||||||
|
$this->content = $content;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityType(): string
|
||||||
|
{
|
||||||
|
return $this->entityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEntityType(string $entityType): static
|
||||||
|
{
|
||||||
|
$this->entityType = $entityType;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityId(): string
|
||||||
|
{
|
||||||
|
return $this->entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEntityId(string $entityId): static
|
||||||
|
{
|
||||||
|
$this->entityId = $entityId;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityName(): ?string
|
||||||
|
{
|
||||||
|
return $this->entityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEntityName(?string $entityName): static
|
||||||
|
{
|
||||||
|
$this->entityName = $entityName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAuthorId(): string
|
||||||
|
{
|
||||||
|
return $this->authorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAuthorId(string $authorId): static
|
||||||
|
{
|
||||||
|
$this->authorId = $authorId;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAuthorName(): string
|
||||||
|
{
|
||||||
|
return $this->authorName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAuthorName(string $authorName): static
|
||||||
|
{
|
||||||
|
$this->authorName = $authorName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): string
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(string $status): static
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResolvedById(): ?string
|
||||||
|
{
|
||||||
|
return $this->resolvedById;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setResolvedById(?string $resolvedById): static
|
||||||
|
{
|
||||||
|
$this->resolvedById = $resolvedById;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResolvedByName(): ?string
|
||||||
|
{
|
||||||
|
return $this->resolvedByName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setResolvedByName(?string $resolvedByName): static
|
||||||
|
{
|
||||||
|
$this->resolvedByName = $resolvedByName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResolvedAt(): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->resolvedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setResolvedAt(?DateTimeImmutable $resolvedAt): static
|
||||||
|
{
|
||||||
|
$this->resolvedAt = $resolvedAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,12 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
|||||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
use ApiPlatform\Metadata\ApiFilter;
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\ComposantRepository;
|
use App\Repository\ComposantRepository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
@@ -22,19 +28,27 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeComposant' => 'exact'])]
|
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeComposant' => 'exact'])]
|
||||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
],
|
||||||
normalizationContext: ['groups' => ['composant:read']],
|
normalizationContext: ['groups' => ['composant:read']],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class Composant
|
class Composant
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
#[Groups(['composant:read'])]
|
#[Groups(['composant:read', 'document:list'])]
|
||||||
private ?string $id = null;
|
private ?string $id = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||||
#[Groups(['composant:read'])]
|
#[Groups(['composant:read', 'document:list'])]
|
||||||
private string $name;
|
private string $name;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||||
@@ -144,7 +158,7 @@ class Composant
|
|||||||
|
|
||||||
public function setName(string $name): static
|
public function setName(string $name): static
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = mb_strtoupper(mb_substr($name, 0, 1)).mb_substr($name, 1);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,35 @@ declare(strict_types=1);
|
|||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\ConstructeurRepository;
|
use App\Repository\ConstructeurRepository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||||
|
|
||||||
|
#[UniqueEntity(fields: ['name'], message: 'Un fournisseur avec ce nom existe déjà.')]
|
||||||
#[ORM\Entity(repositoryClass: ConstructeurRepository::class)]
|
#[ORM\Entity(repositoryClass: ConstructeurRepository::class)]
|
||||||
#[ORM\Table(name: 'constructeurs')]
|
#[ORM\Table(name: 'constructeurs')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class Constructeur
|
class Constructeur
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,38 +5,61 @@ declare(strict_types=1);
|
|||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\CustomFieldRepository;
|
use App\Repository\CustomFieldRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: CustomFieldRepository::class)]
|
#[ORM\Entity(repositoryClass: CustomFieldRepository::class)]
|
||||||
#[ORM\Table(name: 'custom_fields')]
|
#[ORM\Table(name: 'custom_fields')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource]
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
]
|
||||||
|
)]
|
||||||
class CustomField
|
class CustomField
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
private ?string $id = null;
|
private ?string $id = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
private string $name;
|
private string $name;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 50)]
|
#[ORM\Column(type: Types::STRING, length: 50)]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
private string $type;
|
private string $type;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
|
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
private bool $required = false;
|
private bool $required = false;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'defaultValue')]
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'defaultValue')]
|
||||||
private ?string $defaultValue = null;
|
private ?string $defaultValue = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
private ?array $options = null;
|
private ?array $options = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'orderIndex')]
|
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'orderIndex')]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
private int $orderIndex = 0;
|
private int $orderIndex = 0;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: TypeMachine::class, inversedBy: 'customFields')]
|
#[ORM\ManyToOne(targetEntity: TypeMachine::class, inversedBy: 'customFields')]
|
||||||
@@ -62,10 +85,10 @@ class CustomField
|
|||||||
private Collection $customFieldValues;
|
private Collection $customFieldValues;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
private \DateTimeImmutable $createdAt;
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
private \DateTimeImmutable $updatedAt;
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -75,11 +98,11 @@ class CustomField
|
|||||||
#[ORM\PrePersist]
|
#[ORM\PrePersist]
|
||||||
public function setCreatedAtValue(): void
|
public function setCreatedAtValue(): void
|
||||||
{
|
{
|
||||||
$now = new \DateTimeImmutable();
|
$now = new DateTimeImmutable();
|
||||||
$this->createdAt = $now;
|
$this->createdAt = $now;
|
||||||
$this->updatedAt = $now;
|
$this->updatedAt = $now;
|
||||||
|
|
||||||
if ($this->id === null) {
|
if (null === $this->id) {
|
||||||
$this->id = $this->generateCuid();
|
$this->id = $this->generateCuid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,12 +110,7 @@ class CustomField
|
|||||||
#[ORM\PreUpdate]
|
#[ORM\PreUpdate]
|
||||||
public function setUpdatedAtValue(): void
|
public function setUpdatedAtValue(): void
|
||||||
{
|
{
|
||||||
$this->updatedAt = new \DateTimeImmutable();
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
}
|
|
||||||
|
|
||||||
private function generateCuid(): string
|
|
||||||
{
|
|
||||||
return 'cl' . bin2hex(random_bytes(12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?string
|
public function getId(): ?string
|
||||||
@@ -191,13 +209,18 @@ class CustomField
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatedAt(): \DateTimeImmutable
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->createdAt;
|
return $this->createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUpdatedAt(): \DateTimeImmutable
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->updatedAt;
|
return $this->updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,25 +5,45 @@ declare(strict_types=1);
|
|||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\CustomFieldValueRepository;
|
use App\Repository\CustomFieldValueRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: CustomFieldValueRepository::class)]
|
#[ORM\Entity(repositoryClass: CustomFieldValueRepository::class)]
|
||||||
#[ORM\Table(name: 'custom_field_values')]
|
#[ORM\Table(name: 'custom_field_values')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource]
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
]
|
||||||
|
)]
|
||||||
class CustomFieldValue
|
class CustomFieldValue
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
private ?string $id = null;
|
private ?string $id = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
private string $value;
|
private string $value;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: CustomField::class, inversedBy: 'customFieldValues')]
|
#[ORM\ManyToOne(targetEntity: CustomField::class, inversedBy: 'customFieldValues')]
|
||||||
#[ORM\JoinColumn(name: 'customFieldId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
#[ORM\JoinColumn(name: 'customFieldId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
private CustomField $customField;
|
private CustomField $customField;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'customFieldValues')]
|
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'customFieldValues')]
|
||||||
@@ -43,19 +63,21 @@ class CustomFieldValue
|
|||||||
private ?Product $product = null;
|
private ?Product $product = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
private \DateTimeImmutable $createdAt;
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
private \DateTimeImmutable $updatedAt;
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
#[ORM\PrePersist]
|
#[ORM\PrePersist]
|
||||||
public function setCreatedAtValue(): void
|
public function setCreatedAtValue(): void
|
||||||
{
|
{
|
||||||
$now = new \DateTimeImmutable();
|
$now = new DateTimeImmutable();
|
||||||
$this->createdAt = $now;
|
$this->createdAt = $now;
|
||||||
$this->updatedAt = $now;
|
$this->updatedAt = $now;
|
||||||
|
|
||||||
if ($this->id === null) {
|
if (null === $this->id) {
|
||||||
$this->id = $this->generateCuid();
|
$this->id = $this->generateCuid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,12 +85,7 @@ class CustomFieldValue
|
|||||||
#[ORM\PreUpdate]
|
#[ORM\PreUpdate]
|
||||||
public function setUpdatedAtValue(): void
|
public function setUpdatedAtValue(): void
|
||||||
{
|
{
|
||||||
$this->updatedAt = new \DateTimeImmutable();
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
}
|
|
||||||
|
|
||||||
private function generateCuid(): string
|
|
||||||
{
|
|
||||||
return 'cl' . bin2hex(random_bytes(12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?string
|
public function getId(): ?string
|
||||||
@@ -155,13 +172,18 @@ class CustomFieldValue
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatedAt(): \DateTimeImmutable
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->createdAt;
|
return $this->createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUpdatedAt(): \DateTimeImmutable
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->updatedAt;
|
return $this->updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ declare(strict_types=1);
|
|||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\DocumentRepository;
|
use App\Repository\DocumentRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Serializer\Attribute\Groups;
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
@@ -13,68 +19,90 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
#[ORM\Entity(repositoryClass: DocumentRepository::class)]
|
#[ORM\Entity(repositoryClass: DocumentRepository::class)]
|
||||||
#[ORM\Table(name: 'documents')]
|
#[ORM\Table(name: 'documents')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource]
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new GetCollection(
|
||||||
|
security: "is_granted('ROLE_VIEWER')",
|
||||||
|
normalizationContext: ['groups' => ['document:list']],
|
||||||
|
),
|
||||||
|
new Get(
|
||||||
|
security: "is_granted('ROLE_VIEWER')",
|
||||||
|
normalizationContext: ['groups' => ['document:list', 'document:detail']],
|
||||||
|
),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
],
|
||||||
|
paginationClientItemsPerPage: true,
|
||||||
|
paginationMaximumItemsPerPage: 200
|
||||||
|
)]
|
||||||
class Document
|
class Document
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
|
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||||
private ?string $id = null;
|
private ?string $id = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||||
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
|
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||||
private string $name;
|
private string $name;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||||
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
|
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||||
private string $filename;
|
private string $filename;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::TEXT)]
|
#[ORM\Column(type: Types::TEXT)]
|
||||||
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
|
#[Groups(['document:detail', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||||
private string $path;
|
private string $path;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 100, name: 'mimeType')]
|
#[ORM\Column(type: Types::STRING, length: 100, name: 'mimeType')]
|
||||||
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
|
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||||
private string $mimeType;
|
private string $mimeType;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::INTEGER)]
|
#[ORM\Column(type: Types::INTEGER)]
|
||||||
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
|
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||||
private int $size;
|
private int $size;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'documents')]
|
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'documents')]
|
||||||
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
private ?Machine $machine = null;
|
private ?Machine $machine = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'documents')]
|
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'documents')]
|
||||||
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
private ?Composant $composant = null;
|
private ?Composant $composant = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'documents')]
|
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'documents')]
|
||||||
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
private ?Piece $piece = null;
|
private ?Piece $piece = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'documents')]
|
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'documents')]
|
||||||
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
private ?Product $product = null;
|
private ?Product $product = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Site::class, inversedBy: 'documents')]
|
#[ORM\ManyToOne(targetEntity: Site::class, inversedBy: 'documents')]
|
||||||
#[ORM\JoinColumn(name: 'siteId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
#[ORM\JoinColumn(name: 'siteId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
private ?Site $site = null;
|
private ?Site $site = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
private \DateTimeImmutable $createdAt;
|
#[Groups(['document:list'])]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
private \DateTimeImmutable $updatedAt;
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
#[ORM\PrePersist]
|
#[ORM\PrePersist]
|
||||||
public function setCreatedAtValue(): void
|
public function setCreatedAtValue(): void
|
||||||
{
|
{
|
||||||
$now = new \DateTimeImmutable();
|
$now = new DateTimeImmutable();
|
||||||
$this->createdAt = $now;
|
$this->createdAt = $now;
|
||||||
$this->updatedAt = $now;
|
$this->updatedAt = $now;
|
||||||
|
|
||||||
if ($this->id === null) {
|
if (null === $this->id) {
|
||||||
$this->id = $this->generateCuid();
|
$this->id = $this->generateCuid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,12 +110,7 @@ class Document
|
|||||||
#[ORM\PreUpdate]
|
#[ORM\PreUpdate]
|
||||||
public function setUpdatedAtValue(): void
|
public function setUpdatedAtValue(): void
|
||||||
{
|
{
|
||||||
$this->updatedAt = new \DateTimeImmutable();
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
}
|
|
||||||
|
|
||||||
private function generateCuid(): string
|
|
||||||
{
|
|
||||||
return 'cl' . bin2hex(random_bytes(12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?string
|
public function getId(): ?string
|
||||||
@@ -222,13 +245,18 @@ class Document
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatedAt(): \DateTimeImmutable
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->createdAt;
|
return $this->createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUpdatedAt(): \DateTimeImmutable
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->updatedAt;
|
return $this->updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,23 +5,42 @@ declare(strict_types=1);
|
|||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\MachineRepository;
|
use App\Repository\MachineRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: MachineRepository::class)]
|
#[ORM\Entity(repositoryClass: MachineRepository::class)]
|
||||||
#[ORM\Table(name: 'machines')]
|
#[ORM\Table(name: 'machines')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource]
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
]
|
||||||
|
)]
|
||||||
class Machine
|
class Machine
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
private ?string $id = null;
|
private ?string $id = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
private string $name;
|
private string $name;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||||
@@ -80,29 +99,29 @@ class Machine
|
|||||||
private Collection $customFieldValues;
|
private Collection $customFieldValues;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
private \DateTimeImmutable $createdAt;
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
private \DateTimeImmutable $updatedAt;
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->constructeurs = new ArrayCollection();
|
$this->constructeurs = new ArrayCollection();
|
||||||
$this->componentLinks = new ArrayCollection();
|
$this->componentLinks = new ArrayCollection();
|
||||||
$this->pieceLinks = new ArrayCollection();
|
$this->pieceLinks = new ArrayCollection();
|
||||||
$this->productLinks = new ArrayCollection();
|
$this->productLinks = new ArrayCollection();
|
||||||
$this->documents = new ArrayCollection();
|
$this->documents = new ArrayCollection();
|
||||||
$this->customFieldValues = new ArrayCollection();
|
$this->customFieldValues = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ORM\PrePersist]
|
#[ORM\PrePersist]
|
||||||
public function setCreatedAtValue(): void
|
public function setCreatedAtValue(): void
|
||||||
{
|
{
|
||||||
$now = new \DateTimeImmutable();
|
$now = new DateTimeImmutable();
|
||||||
$this->createdAt = $now;
|
$this->createdAt = $now;
|
||||||
$this->updatedAt = $now;
|
$this->updatedAt = $now;
|
||||||
|
|
||||||
if ($this->id === null) {
|
if (null === $this->id) {
|
||||||
$this->id = $this->generateCuid();
|
$this->id = $this->generateCuid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,12 +129,7 @@ class Machine
|
|||||||
#[ORM\PreUpdate]
|
#[ORM\PreUpdate]
|
||||||
public function setUpdatedAtValue(): void
|
public function setUpdatedAtValue(): void
|
||||||
{
|
{
|
||||||
$this->updatedAt = new \DateTimeImmutable();
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
}
|
|
||||||
|
|
||||||
private function generateCuid(): string
|
|
||||||
{
|
|
||||||
return 'cl' . bin2hex(random_bytes(12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?string
|
public function getId(): ?string
|
||||||
@@ -238,13 +252,18 @@ class Machine
|
|||||||
return $this->customFieldValues;
|
return $this->customFieldValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatedAt(): \DateTimeImmutable
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->createdAt;
|
return $this->createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUpdatedAt(): \DateTimeImmutable
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->updatedAt;
|
return $this->updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ declare(strict_types=1);
|
|||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\MachineComponentLinkRepository;
|
use App\Repository\MachineComponentLinkRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
@@ -14,7 +21,16 @@ use Doctrine\ORM\Mapping as ORM;
|
|||||||
#[ORM\Entity(repositoryClass: MachineComponentLinkRepository::class)]
|
#[ORM\Entity(repositoryClass: MachineComponentLinkRepository::class)]
|
||||||
#[ORM\Table(name: 'machine_component_links')]
|
#[ORM\Table(name: 'machine_component_links')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource]
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
]
|
||||||
|
)]
|
||||||
class MachineComponentLink
|
class MachineComponentLink
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
@@ -65,26 +81,26 @@ class MachineComponentLink
|
|||||||
private ?string $prixOverride = null;
|
private ?string $prixOverride = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
private \DateTimeImmutable $createdAt;
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
private \DateTimeImmutable $updatedAt;
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->childLinks = new ArrayCollection();
|
$this->childLinks = new ArrayCollection();
|
||||||
$this->pieceLinks = new ArrayCollection();
|
$this->pieceLinks = new ArrayCollection();
|
||||||
$this->productLinks = new ArrayCollection();
|
$this->productLinks = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ORM\PrePersist]
|
#[ORM\PrePersist]
|
||||||
public function setCreatedAtValue(): void
|
public function setCreatedAtValue(): void
|
||||||
{
|
{
|
||||||
$now = new \DateTimeImmutable();
|
$now = new DateTimeImmutable();
|
||||||
$this->createdAt = $now;
|
$this->createdAt = $now;
|
||||||
$this->updatedAt = $now;
|
$this->updatedAt = $now;
|
||||||
|
|
||||||
if ($this->id === null) {
|
if (null === $this->id) {
|
||||||
$this->id = $this->generateCuid();
|
$this->id = $this->generateCuid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,12 +108,7 @@ class MachineComponentLink
|
|||||||
#[ORM\PreUpdate]
|
#[ORM\PreUpdate]
|
||||||
public function setUpdatedAtValue(): void
|
public function setUpdatedAtValue(): void
|
||||||
{
|
{
|
||||||
$this->updatedAt = new \DateTimeImmutable();
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
}
|
|
||||||
|
|
||||||
private function generateCuid(): string
|
|
||||||
{
|
|
||||||
return 'cl' . bin2hex(random_bytes(12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?string
|
public function getId(): ?string
|
||||||
@@ -195,4 +206,9 @@ class MachineComponentLink
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ declare(strict_types=1);
|
|||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\MachinePieceLinkRepository;
|
use App\Repository\MachinePieceLinkRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
@@ -14,7 +21,16 @@ use Doctrine\ORM\Mapping as ORM;
|
|||||||
#[ORM\Entity(repositoryClass: MachinePieceLinkRepository::class)]
|
#[ORM\Entity(repositoryClass: MachinePieceLinkRepository::class)]
|
||||||
#[ORM\Table(name: 'machine_piece_links')]
|
#[ORM\Table(name: 'machine_piece_links')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource]
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
]
|
||||||
|
)]
|
||||||
class MachinePieceLink
|
class MachinePieceLink
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
@@ -53,10 +69,10 @@ class MachinePieceLink
|
|||||||
private ?string $prixOverride = null;
|
private ?string $prixOverride = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
private \DateTimeImmutable $createdAt;
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
private \DateTimeImmutable $updatedAt;
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -66,11 +82,11 @@ class MachinePieceLink
|
|||||||
#[ORM\PrePersist]
|
#[ORM\PrePersist]
|
||||||
public function setCreatedAtValue(): void
|
public function setCreatedAtValue(): void
|
||||||
{
|
{
|
||||||
$now = new \DateTimeImmutable();
|
$now = new DateTimeImmutable();
|
||||||
$this->createdAt = $now;
|
$this->createdAt = $now;
|
||||||
$this->updatedAt = $now;
|
$this->updatedAt = $now;
|
||||||
|
|
||||||
if ($this->id === null) {
|
if (null === $this->id) {
|
||||||
$this->id = $this->generateCuid();
|
$this->id = $this->generateCuid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,12 +94,7 @@ class MachinePieceLink
|
|||||||
#[ORM\PreUpdate]
|
#[ORM\PreUpdate]
|
||||||
public function setUpdatedAtValue(): void
|
public function setUpdatedAtValue(): void
|
||||||
{
|
{
|
||||||
$this->updatedAt = new \DateTimeImmutable();
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
}
|
|
||||||
|
|
||||||
private function generateCuid(): string
|
|
||||||
{
|
|
||||||
return 'cl' . bin2hex(random_bytes(12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?string
|
public function getId(): ?string
|
||||||
@@ -181,4 +192,9 @@ class MachinePieceLink
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ declare(strict_types=1);
|
|||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\MachineProductLinkRepository;
|
use App\Repository\MachineProductLinkRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
@@ -14,7 +21,16 @@ use Doctrine\ORM\Mapping as ORM;
|
|||||||
#[ORM\Entity(repositoryClass: MachineProductLinkRepository::class)]
|
#[ORM\Entity(repositoryClass: MachineProductLinkRepository::class)]
|
||||||
#[ORM\Table(name: 'machine_product_links')]
|
#[ORM\Table(name: 'machine_product_links')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource]
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
]
|
||||||
|
)]
|
||||||
class MachineProductLink
|
class MachineProductLink
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
@@ -52,10 +68,10 @@ class MachineProductLink
|
|||||||
private ?MachinePieceLink $parentPieceLink = null;
|
private ?MachinePieceLink $parentPieceLink = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
private \DateTimeImmutable $createdAt;
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
private \DateTimeImmutable $updatedAt;
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -65,11 +81,11 @@ class MachineProductLink
|
|||||||
#[ORM\PrePersist]
|
#[ORM\PrePersist]
|
||||||
public function setCreatedAtValue(): void
|
public function setCreatedAtValue(): void
|
||||||
{
|
{
|
||||||
$now = new \DateTimeImmutable();
|
$now = new DateTimeImmutable();
|
||||||
$this->createdAt = $now;
|
$this->createdAt = $now;
|
||||||
$this->updatedAt = $now;
|
$this->updatedAt = $now;
|
||||||
|
|
||||||
if ($this->id === null) {
|
if (null === $this->id) {
|
||||||
$this->id = $this->generateCuid();
|
$this->id = $this->generateCuid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,12 +93,7 @@ class MachineProductLink
|
|||||||
#[ORM\PreUpdate]
|
#[ORM\PreUpdate]
|
||||||
public function setUpdatedAtValue(): void
|
public function setUpdatedAtValue(): void
|
||||||
{
|
{
|
||||||
$this->updatedAt = new \DateTimeImmutable();
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
}
|
|
||||||
|
|
||||||
private function generateCuid(): string
|
|
||||||
{
|
|
||||||
return 'cl' . bin2hex(random_bytes(12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?string
|
public function getId(): ?string
|
||||||
@@ -168,4 +179,9 @@ class MachineProductLink
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,16 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
use ApiPlatform\Metadata\ApiFilter;
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Enum\ModelCategory;
|
use App\Enum\ModelCategory;
|
||||||
use App\Repository\ModelTypeRepository;
|
use App\Repository\ModelTypeRepository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
@@ -21,9 +28,18 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|||||||
#[ORM\UniqueConstraint(name: 'unique_category_name', columns: ['category', 'name'])]
|
#[ORM\UniqueConstraint(name: 'unique_category_name', columns: ['category', 'name'])]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])]
|
#[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])]
|
||||||
|
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class ModelType
|
class ModelType
|
||||||
{
|
{
|
||||||
@@ -178,7 +194,7 @@ class ModelType
|
|||||||
|
|
||||||
public function setName(string $name): static
|
public function setName(string $name): static
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = mb_strtoupper(mb_substr($name, 0, 1)).mb_substr($name, 1);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,36 +8,52 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
|||||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
use ApiPlatform\Metadata\ApiFilter;
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\PieceRepository;
|
use App\Repository\PieceRepository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||||
use Symfony\Component\Serializer\Attribute\Groups;
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[UniqueEntity(fields: ['reference'], message: 'Une pièce avec cette référence existe déjà.')]
|
||||||
#[ORM\Entity(repositoryClass: PieceRepository::class)]
|
#[ORM\Entity(repositoryClass: PieceRepository::class)]
|
||||||
#[ORM\Table(name: 'pieces')]
|
#[ORM\Table(name: 'pieces')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typePiece' => 'exact'])]
|
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typePiece' => 'exact'])]
|
||||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
],
|
||||||
normalizationContext: ['groups' => ['piece:read']],
|
normalizationContext: ['groups' => ['piece:read']],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class Piece
|
class Piece
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
#[Groups(['piece:read'])]
|
#[Groups(['piece:read', 'document:list'])]
|
||||||
private ?string $id = null;
|
private ?string $id = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||||
#[Groups(['piece:read'])]
|
#[Groups(['piece:read', 'document:list'])]
|
||||||
private string $name;
|
private string $name;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
#[ORM\Column(type: Types::STRING, length: 255, unique: true, nullable: true)]
|
||||||
#[Groups(['piece:read'])]
|
#[Groups(['piece:read'])]
|
||||||
private ?string $reference = null;
|
private ?string $reference = null;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
|||||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
use ApiPlatform\Metadata\ApiFilter;
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\ProductRepository;
|
use App\Repository\ProductRepository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
@@ -22,19 +28,27 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeProduct' => 'exact'])]
|
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeProduct' => 'exact'])]
|
||||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
],
|
||||||
normalizationContext: ['groups' => ['product:read']],
|
normalizationContext: ['groups' => ['product:read']],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class Product
|
class Product
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
#[Groups(['product:read'])]
|
#[Groups(['product:read', 'document:list'])]
|
||||||
private ?string $id = null;
|
private ?string $id = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||||
#[Groups(['product:read'])]
|
#[Groups(['product:read', 'document:list'])]
|
||||||
private string $name;
|
private string $name;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ use ApiPlatform\Metadata\ApiResource;
|
|||||||
use ApiPlatform\Metadata\Delete;
|
use ApiPlatform\Metadata\Delete;
|
||||||
use ApiPlatform\Metadata\Get;
|
use ApiPlatform\Metadata\Get;
|
||||||
use ApiPlatform\Metadata\GetCollection;
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
use ApiPlatform\Metadata\Post;
|
use ApiPlatform\Metadata\Post;
|
||||||
use ApiPlatform\Metadata\Put;
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\ProfileRepository;
|
use App\Repository\ProfileRepository;
|
||||||
|
use App\State\ProfilePasswordHasher;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
@@ -24,11 +26,24 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
operations: [
|
operations: [
|
||||||
new Get(),
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
new GetCollection(),
|
new GetCollection(security: "is_granted('ROLE_ADMIN')"),
|
||||||
new Post(),
|
new Post(
|
||||||
new Put(),
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
new Delete(),
|
denormalizationContext: ['groups' => ['profile:write', 'profile:admin:write']],
|
||||||
|
processor: ProfilePasswordHasher::class,
|
||||||
|
),
|
||||||
|
new Put(
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
denormalizationContext: ['groups' => ['profile:write', 'profile:admin:write']],
|
||||||
|
processor: ProfilePasswordHasher::class,
|
||||||
|
),
|
||||||
|
new Patch(
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
denormalizationContext: ['groups' => ['profile:write', 'profile:admin:write']],
|
||||||
|
processor: ProfilePasswordHasher::class,
|
||||||
|
),
|
||||||
|
new Delete(security: "is_granted('ROLE_ADMIN')"),
|
||||||
],
|
],
|
||||||
normalizationContext: ['groups' => ['profile:read']],
|
normalizationContext: ['groups' => ['profile:read']],
|
||||||
denormalizationContext: ['groups' => ['profile:write']]
|
denormalizationContext: ['groups' => ['profile:write']]
|
||||||
@@ -63,16 +78,21 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
* @var list<string> The user roles
|
* @var list<string> The user roles
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(type: 'json', options: ['default' => '["ROLE_USER"]'])]
|
#[ORM\Column(type: 'json', options: ['default' => '["ROLE_USER"]'])]
|
||||||
#[Groups(['profile:read', 'profile:write'])]
|
#[Groups(['profile:read', 'profile:admin:write'])]
|
||||||
private array $roles = ['ROLE_USER'];
|
private array $roles = ['ROLE_USER'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string The hashed password
|
* @var null|string The hashed password
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(type: 'string', nullable: true)]
|
#[ORM\Column(type: 'string', nullable: true)]
|
||||||
#[Groups(['profile:write'])]
|
|
||||||
private ?string $password = null;
|
private ?string $password = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-persisted field used for password hashing via ProfilePasswordHasher.
|
||||||
|
*/
|
||||||
|
#[Groups(['profile:write'])]
|
||||||
|
private ?string $plainPassword = null;
|
||||||
|
|
||||||
#[ORM\Column(type: 'datetime_immutable', name: 'createdat')]
|
#[ORM\Column(type: 'datetime_immutable', name: 'createdat')]
|
||||||
#[Groups(['profile:read'])]
|
#[Groups(['profile:read'])]
|
||||||
private DateTimeImmutable $createdAt;
|
private DateTimeImmutable $createdAt;
|
||||||
@@ -83,7 +103,6 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
// Générer un CUID-like ID pour compatibilité avec Prisma
|
|
||||||
$this->id = 'cl'.substr(strtolower(base_convert(random_bytes(12), 2, 36)), 0, 24);
|
$this->id = 'cl'.substr(strtolower(base_convert(random_bytes(12), 2, 36)), 0, 24);
|
||||||
$this->createdAt = new DateTimeImmutable();
|
$this->createdAt = new DateTimeImmutable();
|
||||||
$this->updatedAt = new DateTimeImmutable();
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
@@ -157,11 +176,10 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
*/
|
*/
|
||||||
public function getRoles(): array
|
public function getRoles(): array
|
||||||
{
|
{
|
||||||
$roles = $this->roles;
|
$roles = $this->roles;
|
||||||
// guarantee every user at least has ROLE_USER
|
|
||||||
$roles[] = 'ROLE_USER';
|
$roles[] = 'ROLE_USER';
|
||||||
|
|
||||||
return array_unique($roles);
|
return array_values(array_unique($roles));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -182,20 +200,37 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
return $this->password;
|
return $this->password;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setPassword(string $password): static
|
public function setPassword(?string $password): static
|
||||||
{
|
{
|
||||||
$this->password = $password;
|
$this->password = $password;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getPlainPassword(): ?string
|
||||||
|
{
|
||||||
|
return $this->plainPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPlainPassword(?string $plainPassword): static
|
||||||
|
{
|
||||||
|
$this->plainPassword = $plainPassword;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Groups(['profile:read'])]
|
||||||
|
public function getHasPassword(): bool
|
||||||
|
{
|
||||||
|
return null !== $this->password && '' !== $this->password;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see UserInterface
|
* @see UserInterface
|
||||||
*/
|
*/
|
||||||
public function eraseCredentials(): void
|
public function eraseCredentials(): void
|
||||||
{
|
{
|
||||||
// If you store any temporary, sensitive data on the user, clear it here
|
$this->plainPassword = null;
|
||||||
// $this->plainPassword = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatedAt(): DateTimeImmutable
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use Doctrine\Common\Collections\ArrayCollection;
|
|||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: SiteRepository::class)]
|
#[ORM\Entity(repositoryClass: SiteRepository::class)]
|
||||||
@@ -23,23 +24,25 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
operations: [
|
operations: [
|
||||||
new Get(),
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
new GetCollection(),
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
new Post(),
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
new Put(),
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
new Delete(),
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
],
|
],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class Site
|
class Site
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
private ?string $id = null;
|
private ?string $id = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||||
#[Assert\NotBlank]
|
#[Assert\NotBlank]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
private string $name;
|
private string $name;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 255, options: ['default' => ''], name: 'contactName')]
|
#[ORM\Column(type: Types::STRING, length: 255, options: ['default' => ''], name: 'contactName')]
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
#[UniqueEntity(fields: ['name'], message: 'Ce nom de type de machine existe déjà.')]
|
#[UniqueEntity(fields: ['name'], message: 'Ce nom de type de machine existe déjà.')]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
operations: [
|
operations: [
|
||||||
new Get(),
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
new GetCollection(),
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
new Post(),
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
new Put(),
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
new Delete(),
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
],
|
],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class TypeMachine
|
class TypeMachine
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ namespace App\Entity;
|
|||||||
|
|
||||||
use ApiPlatform\Metadata\ApiProperty;
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\TypeMachineComponentRequirementRepository;
|
use App\Repository\TypeMachineComponentRequirementRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
@@ -16,7 +23,16 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|||||||
#[ORM\Entity(repositoryClass: TypeMachineComponentRequirementRepository::class)]
|
#[ORM\Entity(repositoryClass: TypeMachineComponentRequirementRepository::class)]
|
||||||
#[ORM\Table(name: 'type_machine_component_requirements')]
|
#[ORM\Table(name: 'type_machine_component_requirements')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource]
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
]
|
||||||
|
)]
|
||||||
class TypeMachineComponentRequirement
|
class TypeMachineComponentRequirement
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
@@ -65,10 +81,10 @@ class TypeMachineComponentRequirement
|
|||||||
private Collection $machineComponentLinks;
|
private Collection $machineComponentLinks;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
private \DateTimeImmutable $createdAt;
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
private \DateTimeImmutable $updatedAt;
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -78,11 +94,11 @@ class TypeMachineComponentRequirement
|
|||||||
#[ORM\PrePersist]
|
#[ORM\PrePersist]
|
||||||
public function setCreatedAtValue(): void
|
public function setCreatedAtValue(): void
|
||||||
{
|
{
|
||||||
$now = new \DateTimeImmutable();
|
$now = new DateTimeImmutable();
|
||||||
$this->createdAt = $now;
|
$this->createdAt = $now;
|
||||||
$this->updatedAt = $now;
|
$this->updatedAt = $now;
|
||||||
|
|
||||||
if ($this->id === null) {
|
if (null === $this->id) {
|
||||||
$this->id = $this->generateCuid();
|
$this->id = $this->generateCuid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,12 +106,7 @@ class TypeMachineComponentRequirement
|
|||||||
#[ORM\PreUpdate]
|
#[ORM\PreUpdate]
|
||||||
public function setUpdatedAtValue(): void
|
public function setUpdatedAtValue(): void
|
||||||
{
|
{
|
||||||
$this->updatedAt = new \DateTimeImmutable();
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
}
|
|
||||||
|
|
||||||
private function generateCuid(): string
|
|
||||||
{
|
|
||||||
return 'cl' . bin2hex(random_bytes(12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?string
|
public function getId(): ?string
|
||||||
@@ -205,4 +216,9 @@ class TypeMachineComponentRequirement
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ namespace App\Entity;
|
|||||||
|
|
||||||
use ApiPlatform\Metadata\ApiProperty;
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\TypeMachinePieceRequirementRepository;
|
use App\Repository\TypeMachinePieceRequirementRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
@@ -16,7 +23,16 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|||||||
#[ORM\Entity(repositoryClass: TypeMachinePieceRequirementRepository::class)]
|
#[ORM\Entity(repositoryClass: TypeMachinePieceRequirementRepository::class)]
|
||||||
#[ORM\Table(name: 'type_machine_piece_requirements')]
|
#[ORM\Table(name: 'type_machine_piece_requirements')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource]
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
]
|
||||||
|
)]
|
||||||
class TypeMachinePieceRequirement
|
class TypeMachinePieceRequirement
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
@@ -65,10 +81,10 @@ class TypeMachinePieceRequirement
|
|||||||
private Collection $machinePieceLinks;
|
private Collection $machinePieceLinks;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
private \DateTimeImmutable $createdAt;
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
private \DateTimeImmutable $updatedAt;
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -78,11 +94,11 @@ class TypeMachinePieceRequirement
|
|||||||
#[ORM\PrePersist]
|
#[ORM\PrePersist]
|
||||||
public function setCreatedAtValue(): void
|
public function setCreatedAtValue(): void
|
||||||
{
|
{
|
||||||
$now = new \DateTimeImmutable();
|
$now = new DateTimeImmutable();
|
||||||
$this->createdAt = $now;
|
$this->createdAt = $now;
|
||||||
$this->updatedAt = $now;
|
$this->updatedAt = $now;
|
||||||
|
|
||||||
if ($this->id === null) {
|
if (null === $this->id) {
|
||||||
$this->id = $this->generateCuid();
|
$this->id = $this->generateCuid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,12 +106,7 @@ class TypeMachinePieceRequirement
|
|||||||
#[ORM\PreUpdate]
|
#[ORM\PreUpdate]
|
||||||
public function setUpdatedAtValue(): void
|
public function setUpdatedAtValue(): void
|
||||||
{
|
{
|
||||||
$this->updatedAt = new \DateTimeImmutable();
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
}
|
|
||||||
|
|
||||||
private function generateCuid(): string
|
|
||||||
{
|
|
||||||
return 'cl' . bin2hex(random_bytes(12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?string
|
public function getId(): ?string
|
||||||
@@ -205,4 +216,9 @@ class TypeMachinePieceRequirement
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ namespace App\Entity;
|
|||||||
|
|
||||||
use ApiPlatform\Metadata\ApiProperty;
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Repository\TypeMachineProductRequirementRepository;
|
use App\Repository\TypeMachineProductRequirementRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
@@ -16,7 +23,16 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|||||||
#[ORM\Entity(repositoryClass: TypeMachineProductRequirementRepository::class)]
|
#[ORM\Entity(repositoryClass: TypeMachineProductRequirementRepository::class)]
|
||||||
#[ORM\Table(name: 'type_machine_product_requirements')]
|
#[ORM\Table(name: 'type_machine_product_requirements')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource]
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
]
|
||||||
|
)]
|
||||||
class TypeMachineProductRequirement
|
class TypeMachineProductRequirement
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
@@ -65,10 +81,10 @@ class TypeMachineProductRequirement
|
|||||||
private Collection $machineProductLinks;
|
private Collection $machineProductLinks;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
private \DateTimeImmutable $createdAt;
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
private \DateTimeImmutable $updatedAt;
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -78,11 +94,11 @@ class TypeMachineProductRequirement
|
|||||||
#[ORM\PrePersist]
|
#[ORM\PrePersist]
|
||||||
public function setCreatedAtValue(): void
|
public function setCreatedAtValue(): void
|
||||||
{
|
{
|
||||||
$now = new \DateTimeImmutable();
|
$now = new DateTimeImmutable();
|
||||||
$this->createdAt = $now;
|
$this->createdAt = $now;
|
||||||
$this->updatedAt = $now;
|
$this->updatedAt = $now;
|
||||||
|
|
||||||
if ($this->id === null) {
|
if (null === $this->id) {
|
||||||
$this->id = $this->generateCuid();
|
$this->id = $this->generateCuid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,12 +106,7 @@ class TypeMachineProductRequirement
|
|||||||
#[ORM\PreUpdate]
|
#[ORM\PreUpdate]
|
||||||
public function setUpdatedAtValue(): void
|
public function setUpdatedAtValue(): void
|
||||||
{
|
{
|
||||||
$this->updatedAt = new \DateTimeImmutable();
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
}
|
|
||||||
|
|
||||||
private function generateCuid(): string
|
|
||||||
{
|
|
||||||
return 'cl' . bin2hex(random_bytes(12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?string
|
public function getId(): ?string
|
||||||
@@ -205,4 +216,9 @@ class TypeMachineProductRequirement
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ namespace App\Enum;
|
|||||||
enum ModelCategory: string
|
enum ModelCategory: string
|
||||||
{
|
{
|
||||||
case COMPONENT = 'COMPONENT';
|
case COMPONENT = 'COMPONENT';
|
||||||
case PIECE = 'PIECE';
|
case PIECE = 'PIECE';
|
||||||
case PRODUCT = 'PRODUCT';
|
case PRODUCT = 'PRODUCT';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ namespace App\EventSubscriber;
|
|||||||
|
|
||||||
use App\Entity\AuditLog;
|
use App\Entity\AuditLog;
|
||||||
use App\Entity\Composant;
|
use App\Entity\Composant;
|
||||||
|
use App\Entity\CustomFieldValue;
|
||||||
use App\Entity\ModelType;
|
use App\Entity\ModelType;
|
||||||
use App\Entity\Product;
|
use App\Entity\Product;
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use DateTimeInterface;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\Common\EventSubscriber;
|
use Doctrine\Common\EventSubscriber;
|
||||||
@@ -15,15 +18,24 @@ use Doctrine\ORM\EntityManagerInterface;
|
|||||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||||
use Doctrine\ORM\Events;
|
use Doctrine\ORM\Events;
|
||||||
use Doctrine\ORM\PersistentCollection;
|
use Doctrine\ORM\PersistentCollection;
|
||||||
|
use Doctrine\ORM\UnitOfWork;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use function is_array;
|
||||||
|
use function is_object;
|
||||||
|
use function is_scalar;
|
||||||
|
use function method_exists;
|
||||||
|
|
||||||
#[AsDoctrineListener(event: Events::onFlush)]
|
#[AsDoctrineListener(event: Events::onFlush)]
|
||||||
final class ComposantAuditSubscriber implements EventSubscriber
|
final class ComposantAuditSubscriber implements EventSubscriber
|
||||||
{
|
{
|
||||||
public function __construct(private readonly RequestStack $requestStack)
|
public function __construct(
|
||||||
{
|
private readonly RequestStack $requestStack,
|
||||||
}
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function getSubscribedEvents(): array
|
public function getSubscribedEvents(): array
|
||||||
{
|
{
|
||||||
@@ -39,10 +51,10 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$uow = $em->getUnitOfWork();
|
$uow = $em->getUnitOfWork();
|
||||||
$actorProfileId = $this->resolveActorProfileId();
|
$actorProfileId = $this->resolveActorProfileId();
|
||||||
$pendingUpdates = [];
|
$pendingUpdates = [];
|
||||||
$pendingSnapshots = [];
|
$pendingSnapshots = [];
|
||||||
$pendingComponents = [];
|
$pendingComponents = [];
|
||||||
|
|
||||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
@@ -50,7 +62,7 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
$snapshot = $this->snapshotComposant($entity);
|
$snapshot = $this->snapshotComposant($entity);
|
||||||
$this->persistAuditLog($em, new AuditLog('composant', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
$this->persistAuditLog($em, new AuditLog('composant', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||||
}
|
}
|
||||||
@@ -61,14 +73,14 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
}
|
}
|
||||||
|
|
||||||
$componentId = (string) $entity->getId();
|
$componentId = (string) $entity->getId();
|
||||||
if ($componentId === '') {
|
if ('' === $componentId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
if ($diff !== []) {
|
if ([] !== $diff) {
|
||||||
$pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff);
|
$pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff);
|
||||||
$pendingSnapshots[$componentId] = $this->snapshotComposant($entity);
|
$pendingSnapshots[$componentId] = $this->snapshotComposant($entity);
|
||||||
$pendingComponents[$componentId] = $entity;
|
$pendingComponents[$componentId] = $entity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,8 +101,10 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingComponents);
|
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingComponents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingComponents);
|
||||||
|
|
||||||
foreach ($pendingUpdates as $componentId => $diff) {
|
foreach ($pendingUpdates as $componentId => $diff) {
|
||||||
if ($diff === []) {
|
if ([] === $diff) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,8 +120,8 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
* @param array<string, Composant> $pendingComponents
|
* @param array<string, Composant> $pendingComponents
|
||||||
*/
|
*/
|
||||||
private function collectCollectionUpdate(
|
private function collectCollectionUpdate(
|
||||||
object $collection,
|
object $collection,
|
||||||
@@ -125,18 +139,18 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
}
|
}
|
||||||
|
|
||||||
$componentId = (string) $owner->getId();
|
$componentId = (string) $owner->getId();
|
||||||
if ($componentId === '') {
|
if ('' === $componentId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$mapping = $collection->getMapping();
|
$mapping = $collection->getMapping();
|
||||||
$fieldName = $mapping['fieldName'] ?? null;
|
$fieldName = $mapping['fieldName'] ?? null;
|
||||||
if ($fieldName !== 'constructeurs') {
|
if ('constructeurs' !== $fieldName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$before = $this->normalizeCollection($collection->getSnapshot());
|
$before = $this->normalizeCollection($collection->getSnapshot());
|
||||||
$after = $this->normalizeCollection($collection->toArray());
|
$after = $this->normalizeCollection($collection->toArray());
|
||||||
|
|
||||||
if ($before === $after) {
|
if ($before === $after) {
|
||||||
return;
|
return;
|
||||||
@@ -145,15 +159,84 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
$diff = [
|
$diff = [
|
||||||
'constructeurIds' => [
|
'constructeurIds' => [
|
||||||
'from' => $before,
|
'from' => $before,
|
||||||
'to' => $after,
|
'to' => $after,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff);
|
$pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff);
|
||||||
$pendingSnapshots[$componentId] = $this->snapshotComposant($owner);
|
$pendingSnapshots[$componentId] = $this->snapshotComposant($owner);
|
||||||
$pendingComponents[$componentId] = $owner;
|
$pendingComponents[$componentId] = $owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
|
* @param array<string, Composant> $pendingComponents
|
||||||
|
*/
|
||||||
|
private function collectCustomFieldValueChanges(
|
||||||
|
UnitOfWork $uow,
|
||||||
|
array &$pendingUpdates,
|
||||||
|
array &$pendingSnapshots,
|
||||||
|
array &$pendingComponents,
|
||||||
|
): void {
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if ($entity instanceof CustomFieldValue) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingComponents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof CustomFieldValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$changeSet = $uow->getEntityChangeSet($entity);
|
||||||
|
if (!isset($changeSet['value'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
[$oldVal, $newVal] = $changeSet['value'];
|
||||||
|
if ($oldVal !== $newVal) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, $oldVal, $newVal, $pendingUpdates, $pendingSnapshots, $pendingComponents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if ($entity instanceof CustomFieldValue) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingComponents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
|
* @param array<string, Composant> $pendingComponents
|
||||||
|
*/
|
||||||
|
private function trackCustomFieldValueChange(
|
||||||
|
CustomFieldValue $cfv,
|
||||||
|
mixed $from,
|
||||||
|
mixed $to,
|
||||||
|
array &$pendingUpdates,
|
||||||
|
array &$pendingSnapshots,
|
||||||
|
array &$pendingComponents,
|
||||||
|
): void {
|
||||||
|
$owner = $cfv->getComposant();
|
||||||
|
if (!$owner instanceof Composant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ownerId = (string) $owner->getId();
|
||||||
|
if ('' === $ownerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldName = 'customField:'.$cfv->getCustomField()->getName();
|
||||||
|
$diff = [$fieldName => ['from' => $from, 'to' => $to]];
|
||||||
|
|
||||||
|
$pendingUpdates[$ownerId] = $this->mergeDiffs($pendingUpdates[$ownerId] ?? [], $diff);
|
||||||
|
$pendingSnapshots[$ownerId] = $this->snapshotComposant($owner);
|
||||||
|
$pendingComponents[$ownerId] = $owner;
|
||||||
|
}
|
||||||
|
|
||||||
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||||
{
|
{
|
||||||
$uow = $em->getUnitOfWork();
|
$uow = $em->getUnitOfWork();
|
||||||
@@ -166,13 +249,14 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
||||||
|
*
|
||||||
* @return array<string, array{from:mixed, to:mixed}>
|
* @return array<string, array{from:mixed, to:mixed}>
|
||||||
*/
|
*/
|
||||||
private function buildDiffFromChangeSet(array $changeSet): array
|
private function buildDiffFromChangeSet(array $changeSet): array
|
||||||
{
|
{
|
||||||
$diff = [];
|
$diff = [];
|
||||||
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
||||||
if ($field === 'updatedAt' || $field === 'createdAt') {
|
if ('updatedAt' === $field || 'createdAt' === $field) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +269,7 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
$diff[$field] = [
|
$diff[$field] = [
|
||||||
'from' => $normalizedOld,
|
'from' => $normalizedOld,
|
||||||
'to' => $normalizedNew,
|
'to' => $normalizedNew,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,51 +279,57 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
private function snapshotComposant(Composant $component): array
|
private function snapshotComposant(Composant $component): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $component->getId(),
|
'id' => $component->getId(),
|
||||||
'name' => $component->getName(),
|
'name' => $component->getName(),
|
||||||
'reference' => $component->getReference(),
|
'reference' => $component->getReference(),
|
||||||
'prix' => $component->getPrix(),
|
'prix' => $component->getPrix(),
|
||||||
'structure' => $component->getStructure(),
|
'structure' => $component->getStructure(),
|
||||||
'typeComposant' => $this->normalizeValue($component->getTypeComposant()),
|
'typeComposant' => $this->normalizeValue($component->getTypeComposant()),
|
||||||
'product' => $this->normalizeValue($component->getProduct()),
|
'product' => $this->normalizeValue($component->getProduct()),
|
||||||
'constructeurIds' => $this->normalizeCollection($component->getConstructeurs()),
|
'constructeurIds' => $this->normalizeCollection($component->getConstructeurs()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param iterable<mixed> $items
|
* @param iterable<mixed> $items
|
||||||
* @return list<string>
|
*
|
||||||
|
* @return list<array{id: string, name: string}|string>
|
||||||
*/
|
*/
|
||||||
private function normalizeCollection(iterable $items): array
|
private function normalizeCollection(iterable $items): array
|
||||||
{
|
{
|
||||||
$ids = [];
|
$entries = [];
|
||||||
|
$seen = [];
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
if (\is_object($item) && \method_exists($item, 'getId')) {
|
if (is_object($item) && method_exists($item, 'getId')) {
|
||||||
$id = $item->getId();
|
$id = $item->getId();
|
||||||
if ($id !== null && $id !== '') {
|
if (null === $id || '' === $id || isset($seen[(string) $id])) {
|
||||||
$ids[] = (string) $id;
|
continue;
|
||||||
|
}
|
||||||
|
$seen[(string) $id] = true;
|
||||||
|
if (method_exists($item, 'getName')) {
|
||||||
|
$entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
|
||||||
|
} else {
|
||||||
|
$entries[] = (string) $id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort($ids);
|
return $entries;
|
||||||
|
|
||||||
return array_values(array_unique($ids));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeValue(mixed $value): mixed
|
private function normalizeValue(mixed $value): mixed
|
||||||
{
|
{
|
||||||
if ($value === null || \is_scalar($value)) {
|
if (null === $value || is_scalar($value)) {
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($value instanceof \DateTimeInterface) {
|
if ($value instanceof DateTimeInterface) {
|
||||||
return $value->format(\DateTimeInterface::ATOM);
|
return $value->format(DateTimeInterface::ATOM);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($value instanceof ModelType) {
|
if ($value instanceof ModelType) {
|
||||||
return [
|
return [
|
||||||
'id' => $value->getId(),
|
'id' => $value->getId(),
|
||||||
'name' => $value->getName(),
|
'name' => $value->getName(),
|
||||||
'code' => $value->getCode(),
|
'code' => $value->getCode(),
|
||||||
];
|
];
|
||||||
@@ -247,8 +337,8 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
if ($value instanceof Product) {
|
if ($value instanceof Product) {
|
||||||
return [
|
return [
|
||||||
'id' => $value->getId(),
|
'id' => $value->getId(),
|
||||||
'name' => $value->getName(),
|
'name' => $value->getName(),
|
||||||
'reference' => $value->getReference(),
|
'reference' => $value->getReference(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -257,11 +347,11 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
return $this->normalizeCollection($value);
|
return $this->normalizeCollection($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (\is_object($value) && \method_exists($value, 'getId')) {
|
if (is_object($value) && method_exists($value, 'getId')) {
|
||||||
return (string) $value->getId();
|
return (string) $value->getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (\is_array($value)) {
|
if (is_array($value)) {
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +361,7 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
/**
|
/**
|
||||||
* @param array<string, array{from:mixed, to:mixed}> $base
|
* @param array<string, array{from:mixed, to:mixed}> $base
|
||||||
* @param array<string, array{from:mixed, to:mixed}> $extra
|
* @param array<string, array{from:mixed, to:mixed}> $extra
|
||||||
|
*
|
||||||
* @return array<string, array{from:mixed, to:mixed}>
|
* @return array<string, array{from:mixed, to:mixed}>
|
||||||
*/
|
*/
|
||||||
private function mergeDiffs(array $base, array $extra): array
|
private function mergeDiffs(array $base, array $extra): array
|
||||||
@@ -284,17 +375,23 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
private function resolveActorProfileId(): ?string
|
private function resolveActorProfileId(): ?string
|
||||||
{
|
{
|
||||||
$session = $this->requestStack->getSession();
|
try {
|
||||||
if (!$session instanceof SessionInterface) {
|
$session = $this->requestStack->getSession();
|
||||||
return null;
|
if ($session instanceof SessionInterface) {
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if ($profileId) {
|
||||||
|
return (string) $profileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
// No session available (CLI context, etc.)
|
||||||
}
|
}
|
||||||
|
|
||||||
$profileId = $session->get('profileId');
|
$user = $this->security->getUser();
|
||||||
if (!$profileId) {
|
if ($user instanceof Profile) {
|
||||||
return null;
|
return $user->getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (string) $profileId;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
168
src/EventSubscriber/ConstructeurAuditSubscriber.php
Normal file
168
src/EventSubscriber/ConstructeurAuditSubscriber.php
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Entity\AuditLog;
|
||||||
|
use App\Entity\Constructeur;
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
|
use Doctrine\Common\EventSubscriber;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||||
|
use Doctrine\ORM\Events;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use function is_scalar;
|
||||||
|
|
||||||
|
#[AsDoctrineListener(event: Events::onFlush)]
|
||||||
|
final class ConstructeurAuditSubscriber implements EventSubscriber
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Events::onFlush,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onFlush(OnFlushEventArgs $args): void
|
||||||
|
{
|
||||||
|
$em = $args->getObjectManager();
|
||||||
|
if (!$em instanceof EntityManagerInterface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$actorProfileId = $this->resolveActorProfileId();
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if (!$entity instanceof Constructeur) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
$snapshot = $this->snapshotConstructeur($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('constructeur', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof Constructeur) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (string) $entity->getId();
|
||||||
|
if ('' === $id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
if ([] !== $diff) {
|
||||||
|
$snapshot = $this->snapshotConstructeur($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('constructeur', $id, 'update', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if (!$entity instanceof Constructeur) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot = $this->snapshotConstructeur($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('constructeur', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||||
|
{
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$log->initializeAuditLog();
|
||||||
|
$em->persist($log);
|
||||||
|
|
||||||
|
$meta = $em->getClassMetadata(AuditLog::class);
|
||||||
|
$uow->computeChangeSet($meta, $log);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
||||||
|
*
|
||||||
|
* @return array<string, array{from:mixed, to:mixed}>
|
||||||
|
*/
|
||||||
|
private function buildDiffFromChangeSet(array $changeSet): array
|
||||||
|
{
|
||||||
|
$diff = [];
|
||||||
|
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
||||||
|
if ('updatedAt' === $field || 'createdAt' === $field) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedOld = $this->normalizeValue($oldValue);
|
||||||
|
$normalizedNew = $this->normalizeValue($newValue);
|
||||||
|
|
||||||
|
if ($normalizedOld === $normalizedNew) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff[$field] = [
|
||||||
|
'from' => $normalizedOld,
|
||||||
|
'to' => $normalizedNew,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function snapshotConstructeur(Constructeur $constructeur): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $constructeur->getId(),
|
||||||
|
'name' => $constructeur->getName(),
|
||||||
|
'email' => $constructeur->getEmail(),
|
||||||
|
'phone' => $constructeur->getPhone(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeValue(mixed $value): mixed
|
||||||
|
{
|
||||||
|
if (null === $value || is_scalar($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof DateTimeInterface) {
|
||||||
|
return $value->format(DateTimeInterface::ATOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveActorProfileId(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$session = $this->requestStack->getSession();
|
||||||
|
if ($session instanceof SessionInterface) {
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if ($profileId) {
|
||||||
|
return (string) $profileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
// No session available (CLI context, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if ($user instanceof Profile) {
|
||||||
|
return $user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
192
src/EventSubscriber/DocumentAuditSubscriber.php
Normal file
192
src/EventSubscriber/DocumentAuditSubscriber.php
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Entity\AuditLog;
|
||||||
|
use App\Entity\Composant;
|
||||||
|
use App\Entity\Document;
|
||||||
|
use App\Entity\Machine;
|
||||||
|
use App\Entity\Piece;
|
||||||
|
use App\Entity\Product;
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use App\Entity\Site;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
|
use Doctrine\Common\EventSubscriber;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||||
|
use Doctrine\ORM\Events;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use function is_object;
|
||||||
|
use function is_scalar;
|
||||||
|
use function method_exists;
|
||||||
|
|
||||||
|
#[AsDoctrineListener(event: Events::onFlush)]
|
||||||
|
final class DocumentAuditSubscriber implements EventSubscriber
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Events::onFlush,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onFlush(OnFlushEventArgs $args): void
|
||||||
|
{
|
||||||
|
$em = $args->getObjectManager();
|
||||||
|
if (!$em instanceof EntityManagerInterface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$actorProfileId = $this->resolveActorProfileId();
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if (!$entity instanceof Document) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
$snapshot = $this->snapshotDocument($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('document', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof Document) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (string) $entity->getId();
|
||||||
|
if ('' === $id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
if ([] !== $diff) {
|
||||||
|
$snapshot = $this->snapshotDocument($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('document', $id, 'update', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if (!$entity instanceof Document) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot = $this->snapshotDocument($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('document', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||||
|
{
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$log->initializeAuditLog();
|
||||||
|
$em->persist($log);
|
||||||
|
|
||||||
|
$meta = $em->getClassMetadata(AuditLog::class);
|
||||||
|
$uow->computeChangeSet($meta, $log);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
||||||
|
*
|
||||||
|
* @return array<string, array{from:mixed, to:mixed}>
|
||||||
|
*/
|
||||||
|
private function buildDiffFromChangeSet(array $changeSet): array
|
||||||
|
{
|
||||||
|
$diff = [];
|
||||||
|
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
||||||
|
if ('updatedAt' === $field || 'createdAt' === $field) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedOld = $this->normalizeValue($oldValue);
|
||||||
|
$normalizedNew = $this->normalizeValue($newValue);
|
||||||
|
|
||||||
|
if ($normalizedOld === $normalizedNew) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff[$field] = [
|
||||||
|
'from' => $normalizedOld,
|
||||||
|
'to' => $normalizedNew,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function snapshotDocument(Document $document): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $document->getId(),
|
||||||
|
'name' => $document->getName(),
|
||||||
|
'filename' => $document->getFilename(),
|
||||||
|
'mimeType' => $document->getMimeType(),
|
||||||
|
'size' => $document->getSize(),
|
||||||
|
'machine' => $this->normalizeValue($document->getMachine()),
|
||||||
|
'composant' => $this->normalizeValue($document->getComposant()),
|
||||||
|
'piece' => $this->normalizeValue($document->getPiece()),
|
||||||
|
'product' => $this->normalizeValue($document->getProduct()),
|
||||||
|
'site' => $this->normalizeValue($document->getSite()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeValue(mixed $value): mixed
|
||||||
|
{
|
||||||
|
if (null === $value || is_scalar($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof DateTimeInterface) {
|
||||||
|
return $value->format(DateTimeInterface::ATOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof Machine || $value instanceof Composant || $value instanceof Piece || $value instanceof Product || $value instanceof Site) {
|
||||||
|
return [
|
||||||
|
'id' => $value->getId(),
|
||||||
|
'name' => $value->getName(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_object($value) && method_exists($value, 'getId')) {
|
||||||
|
return (string) $value->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveActorProfileId(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$session = $this->requestStack->getSession();
|
||||||
|
if ($session instanceof SessionInterface) {
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if ($profileId) {
|
||||||
|
return (string) $profileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
// No session available (CLI context, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if ($user instanceof Profile) {
|
||||||
|
return $user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
412
src/EventSubscriber/MachineAuditSubscriber.php
Normal file
412
src/EventSubscriber/MachineAuditSubscriber.php
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Entity\AuditLog;
|
||||||
|
use App\Entity\CustomFieldValue;
|
||||||
|
use App\Entity\Machine;
|
||||||
|
use App\Entity\ModelType;
|
||||||
|
use App\Entity\Product;
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use App\Entity\Site;
|
||||||
|
use App\Entity\TypeMachine;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\Common\EventSubscriber;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||||
|
use Doctrine\ORM\Events;
|
||||||
|
use Doctrine\ORM\PersistentCollection;
|
||||||
|
use Doctrine\ORM\UnitOfWork;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use function is_array;
|
||||||
|
use function is_object;
|
||||||
|
use function is_scalar;
|
||||||
|
use function method_exists;
|
||||||
|
|
||||||
|
#[AsDoctrineListener(event: Events::onFlush)]
|
||||||
|
final class MachineAuditSubscriber implements EventSubscriber
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Events::onFlush,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onFlush(OnFlushEventArgs $args): void
|
||||||
|
{
|
||||||
|
$em = $args->getObjectManager();
|
||||||
|
if (!$em instanceof EntityManagerInterface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$actorProfileId = $this->resolveActorProfileId();
|
||||||
|
$pendingUpdates = [];
|
||||||
|
$pendingSnapshots = [];
|
||||||
|
$pendingMachines = [];
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if (!$entity instanceof Machine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
$snapshot = $this->snapshotMachine($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('machine', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof Machine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$machineId = (string) $entity->getId();
|
||||||
|
if ('' === $machineId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
if ([] !== $diff) {
|
||||||
|
$pendingUpdates[$machineId] = $this->mergeDiffs($pendingUpdates[$machineId] ?? [], $diff);
|
||||||
|
$pendingSnapshots[$machineId] = $this->snapshotMachine($entity);
|
||||||
|
$pendingMachines[$machineId] = $entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if (!$entity instanceof Machine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot = $this->snapshotMachine($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('machine', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledCollectionUpdates() as $collection) {
|
||||||
|
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||||
|
}
|
||||||
|
foreach ($uow->getScheduledCollectionDeletions() as $collection) {
|
||||||
|
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||||
|
|
||||||
|
foreach ($pendingUpdates as $machineId => $diff) {
|
||||||
|
if ([] === $diff) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$machine = $pendingMachines[$machineId] ?? null;
|
||||||
|
if (!$machine instanceof Machine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot = $pendingSnapshots[$machineId] ?? $this->snapshotMachine($machine);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('machine', $machineId, 'update', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
|
* @param array<string, Machine> $pendingMachines
|
||||||
|
*/
|
||||||
|
private function collectCollectionUpdate(
|
||||||
|
object $collection,
|
||||||
|
array &$pendingUpdates,
|
||||||
|
array &$pendingSnapshots,
|
||||||
|
array &$pendingMachines,
|
||||||
|
): void {
|
||||||
|
if (!$collection instanceof PersistentCollection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$owner = $collection->getOwner();
|
||||||
|
if (!$owner instanceof Machine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$machineId = (string) $owner->getId();
|
||||||
|
if ('' === $machineId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapping = $collection->getMapping();
|
||||||
|
$fieldName = $mapping['fieldName'] ?? null;
|
||||||
|
if ('constructeurs' !== $fieldName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = $this->normalizeCollection($collection->getSnapshot());
|
||||||
|
$after = $this->normalizeCollection($collection->toArray());
|
||||||
|
|
||||||
|
if ($before === $after) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = [
|
||||||
|
'constructeurIds' => [
|
||||||
|
'from' => $before,
|
||||||
|
'to' => $after,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$pendingUpdates[$machineId] = $this->mergeDiffs($pendingUpdates[$machineId] ?? [], $diff);
|
||||||
|
$pendingSnapshots[$machineId] = $this->snapshotMachine($owner);
|
||||||
|
$pendingMachines[$machineId] = $owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
|
* @param array<string, Machine> $pendingMachines
|
||||||
|
*/
|
||||||
|
private function collectCustomFieldValueChanges(
|
||||||
|
UnitOfWork $uow,
|
||||||
|
array &$pendingUpdates,
|
||||||
|
array &$pendingSnapshots,
|
||||||
|
array &$pendingMachines,
|
||||||
|
): void {
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if ($entity instanceof CustomFieldValue) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof CustomFieldValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$changeSet = $uow->getEntityChangeSet($entity);
|
||||||
|
if (!isset($changeSet['value'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
[$oldVal, $newVal] = $changeSet['value'];
|
||||||
|
if ($oldVal !== $newVal) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, $oldVal, $newVal, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if ($entity instanceof CustomFieldValue) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
|
* @param array<string, Machine> $pendingMachines
|
||||||
|
*/
|
||||||
|
private function trackCustomFieldValueChange(
|
||||||
|
CustomFieldValue $cfv,
|
||||||
|
mixed $from,
|
||||||
|
mixed $to,
|
||||||
|
array &$pendingUpdates,
|
||||||
|
array &$pendingSnapshots,
|
||||||
|
array &$pendingMachines,
|
||||||
|
): void {
|
||||||
|
$owner = $cfv->getMachine();
|
||||||
|
if (!$owner instanceof Machine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ownerId = (string) $owner->getId();
|
||||||
|
if ('' === $ownerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldName = 'customField:'.$cfv->getCustomField()->getName();
|
||||||
|
$diff = [$fieldName => ['from' => $from, 'to' => $to]];
|
||||||
|
|
||||||
|
$pendingUpdates[$ownerId] = $this->mergeDiffs($pendingUpdates[$ownerId] ?? [], $diff);
|
||||||
|
$pendingSnapshots[$ownerId] = $this->snapshotMachine($owner);
|
||||||
|
$pendingMachines[$ownerId] = $owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||||
|
{
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$log->initializeAuditLog();
|
||||||
|
$em->persist($log);
|
||||||
|
|
||||||
|
$meta = $em->getClassMetadata(AuditLog::class);
|
||||||
|
$uow->computeChangeSet($meta, $log);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
||||||
|
*
|
||||||
|
* @return array<string, array{from:mixed, to:mixed}>
|
||||||
|
*/
|
||||||
|
private function buildDiffFromChangeSet(array $changeSet): array
|
||||||
|
{
|
||||||
|
$diff = [];
|
||||||
|
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
||||||
|
if ('updatedAt' === $field || 'createdAt' === $field) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedOld = $this->normalizeValue($oldValue);
|
||||||
|
$normalizedNew = $this->normalizeValue($newValue);
|
||||||
|
|
||||||
|
if ($normalizedOld === $normalizedNew) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff[$field] = [
|
||||||
|
'from' => $normalizedOld,
|
||||||
|
'to' => $normalizedNew,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function snapshotMachine(Machine $machine): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $machine->getId(),
|
||||||
|
'name' => $machine->getName(),
|
||||||
|
'reference' => $machine->getReference(),
|
||||||
|
'prix' => $machine->getPrix(),
|
||||||
|
'site' => $this->normalizeValue($machine->getSite()),
|
||||||
|
'typeMachine' => $this->normalizeValue($machine->getTypeMachine()),
|
||||||
|
'constructeurIds' => $this->normalizeCollection($machine->getConstructeurs()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param iterable<mixed> $items
|
||||||
|
*
|
||||||
|
* @return list<array{id: string, name: string}|string>
|
||||||
|
*/
|
||||||
|
private function normalizeCollection(iterable $items): array
|
||||||
|
{
|
||||||
|
$entries = [];
|
||||||
|
$seen = [];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (is_object($item) && method_exists($item, 'getId')) {
|
||||||
|
$id = $item->getId();
|
||||||
|
if (null === $id || '' === $id || isset($seen[(string) $id])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seen[(string) $id] = true;
|
||||||
|
if (method_exists($item, 'getName')) {
|
||||||
|
$entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
|
||||||
|
} else {
|
||||||
|
$entries[] = (string) $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeValue(mixed $value): mixed
|
||||||
|
{
|
||||||
|
if (null === $value || is_scalar($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof DateTimeInterface) {
|
||||||
|
return $value->format(DateTimeInterface::ATOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof Site) {
|
||||||
|
return [
|
||||||
|
'id' => $value->getId(),
|
||||||
|
'name' => $value->getName(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof TypeMachine) {
|
||||||
|
return [
|
||||||
|
'id' => $value->getId(),
|
||||||
|
'name' => $value->getName(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof ModelType) {
|
||||||
|
return [
|
||||||
|
'id' => $value->getId(),
|
||||||
|
'name' => $value->getName(),
|
||||||
|
'code' => $value->getCode(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof Product) {
|
||||||
|
return [
|
||||||
|
'id' => $value->getId(),
|
||||||
|
'name' => $value->getName(),
|
||||||
|
'reference' => $value->getReference(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof Collection) {
|
||||||
|
return $this->normalizeCollection($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_object($value) && method_exists($value, 'getId')) {
|
||||||
|
return (string) $value->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array{from:mixed, to:mixed}> $base
|
||||||
|
* @param array<string, array{from:mixed, to:mixed}> $extra
|
||||||
|
*
|
||||||
|
* @return array<string, array{from:mixed, to:mixed}>
|
||||||
|
*/
|
||||||
|
private function mergeDiffs(array $base, array $extra): array
|
||||||
|
{
|
||||||
|
foreach ($extra as $field => $change) {
|
||||||
|
$base[$field] = $change;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $base;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveActorProfileId(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$session = $this->requestStack->getSession();
|
||||||
|
if ($session instanceof SessionInterface) {
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if ($profileId) {
|
||||||
|
return (string) $profileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
// No session available (CLI context, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if ($user instanceof Profile) {
|
||||||
|
return $user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
180
src/EventSubscriber/ModelTypeAuditSubscriber.php
Normal file
180
src/EventSubscriber/ModelTypeAuditSubscriber.php
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Entity\AuditLog;
|
||||||
|
use App\Entity\ModelType;
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use App\Enum\ModelCategory;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
|
use Doctrine\Common\EventSubscriber;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||||
|
use Doctrine\ORM\Events;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use function is_array;
|
||||||
|
use function is_scalar;
|
||||||
|
|
||||||
|
#[AsDoctrineListener(event: Events::onFlush)]
|
||||||
|
final class ModelTypeAuditSubscriber implements EventSubscriber
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Events::onFlush,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onFlush(OnFlushEventArgs $args): void
|
||||||
|
{
|
||||||
|
$em = $args->getObjectManager();
|
||||||
|
if (!$em instanceof EntityManagerInterface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$actorProfileId = $this->resolveActorProfileId();
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if (!$entity instanceof ModelType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
$snapshot = $this->snapshotModelType($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('model_type', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof ModelType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (string) $entity->getId();
|
||||||
|
if ('' === $id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
if ([] !== $diff) {
|
||||||
|
$snapshot = $this->snapshotModelType($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('model_type', $id, 'update', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if (!$entity instanceof ModelType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot = $this->snapshotModelType($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('model_type', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||||
|
{
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$log->initializeAuditLog();
|
||||||
|
$em->persist($log);
|
||||||
|
|
||||||
|
$meta = $em->getClassMetadata(AuditLog::class);
|
||||||
|
$uow->computeChangeSet($meta, $log);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
||||||
|
*
|
||||||
|
* @return array<string, array{from:mixed, to:mixed}>
|
||||||
|
*/
|
||||||
|
private function buildDiffFromChangeSet(array $changeSet): array
|
||||||
|
{
|
||||||
|
$diff = [];
|
||||||
|
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
||||||
|
if ('updatedAt' === $field || 'createdAt' === $field) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedOld = $this->normalizeValue($oldValue);
|
||||||
|
$normalizedNew = $this->normalizeValue($newValue);
|
||||||
|
|
||||||
|
if ($normalizedOld === $normalizedNew) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff[$field] = [
|
||||||
|
'from' => $normalizedOld,
|
||||||
|
'to' => $normalizedNew,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function snapshotModelType(ModelType $modelType): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $modelType->getId(),
|
||||||
|
'name' => $modelType->getName(),
|
||||||
|
'code' => $modelType->getCode(),
|
||||||
|
'category' => $modelType->getCategory()->value,
|
||||||
|
'notes' => $modelType->getNotes(),
|
||||||
|
'description' => $modelType->getDescription(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeValue(mixed $value): mixed
|
||||||
|
{
|
||||||
|
if (null === $value || is_scalar($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof DateTimeInterface) {
|
||||||
|
return $value->format(DateTimeInterface::ATOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof ModelCategory) {
|
||||||
|
return $value->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveActorProfileId(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$session = $this->requestStack->getSession();
|
||||||
|
if ($session instanceof SessionInterface) {
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if ($profileId) {
|
||||||
|
return (string) $profileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
// No session available (CLI context, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if ($user instanceof Profile) {
|
||||||
|
return $user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,12 @@ declare(strict_types=1);
|
|||||||
namespace App\EventSubscriber;
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
use App\Entity\AuditLog;
|
use App\Entity\AuditLog;
|
||||||
|
use App\Entity\CustomFieldValue;
|
||||||
use App\Entity\ModelType;
|
use App\Entity\ModelType;
|
||||||
use App\Entity\Piece;
|
use App\Entity\Piece;
|
||||||
use App\Entity\Product;
|
use App\Entity\Product;
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use DateTimeInterface;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\Common\EventSubscriber;
|
use Doctrine\Common\EventSubscriber;
|
||||||
@@ -15,15 +18,24 @@ use Doctrine\ORM\EntityManagerInterface;
|
|||||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||||
use Doctrine\ORM\Events;
|
use Doctrine\ORM\Events;
|
||||||
use Doctrine\ORM\PersistentCollection;
|
use Doctrine\ORM\PersistentCollection;
|
||||||
|
use Doctrine\ORM\UnitOfWork;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use function is_array;
|
||||||
|
use function is_object;
|
||||||
|
use function is_scalar;
|
||||||
|
use function method_exists;
|
||||||
|
|
||||||
#[AsDoctrineListener(event: Events::onFlush)]
|
#[AsDoctrineListener(event: Events::onFlush)]
|
||||||
final class PieceAuditSubscriber implements EventSubscriber
|
final class PieceAuditSubscriber implements EventSubscriber
|
||||||
{
|
{
|
||||||
public function __construct(private readonly RequestStack $requestStack)
|
public function __construct(
|
||||||
{
|
private readonly RequestStack $requestStack,
|
||||||
}
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function getSubscribedEvents(): array
|
public function getSubscribedEvents(): array
|
||||||
{
|
{
|
||||||
@@ -39,18 +51,18 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$uow = $em->getUnitOfWork();
|
$uow = $em->getUnitOfWork();
|
||||||
$actorProfileId = $this->resolveActorProfileId();
|
$actorProfileId = $this->resolveActorProfileId();
|
||||||
$pendingUpdates = [];
|
$pendingUpdates = [];
|
||||||
$pendingSnapshots = [];
|
$pendingSnapshots = [];
|
||||||
$pendingPieces = [];
|
$pendingPieces = [];
|
||||||
|
|
||||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
if (!$entity instanceof Piece) {
|
if (!$entity instanceof Piece) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
$snapshot = $this->snapshotPiece($entity);
|
$snapshot = $this->snapshotPiece($entity);
|
||||||
$this->persistAuditLog($em, new AuditLog('piece', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
$this->persistAuditLog($em, new AuditLog('piece', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||||
}
|
}
|
||||||
@@ -61,15 +73,15 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
}
|
}
|
||||||
|
|
||||||
$pieceId = (string) $entity->getId();
|
$pieceId = (string) $entity->getId();
|
||||||
if ($pieceId === '') {
|
if ('' === $pieceId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
if ($diff !== []) {
|
if ([] !== $diff) {
|
||||||
$pendingUpdates[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff);
|
$pendingUpdates[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff);
|
||||||
$pendingSnapshots[$pieceId] = $this->snapshotPiece($entity);
|
$pendingSnapshots[$pieceId] = $this->snapshotPiece($entity);
|
||||||
$pendingPieces[$pieceId] = $entity;
|
$pendingPieces[$pieceId] = $entity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,8 +101,10 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingPieces);
|
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingPieces);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingPieces);
|
||||||
|
|
||||||
foreach ($pendingUpdates as $pieceId => $diff) {
|
foreach ($pendingUpdates as $pieceId => $diff) {
|
||||||
if ($diff === []) {
|
if ([] === $diff) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,8 +120,8 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
* @param array<string, Piece> $pendingPieces
|
* @param array<string, Piece> $pendingPieces
|
||||||
*/
|
*/
|
||||||
private function collectCollectionUpdate(
|
private function collectCollectionUpdate(
|
||||||
object $collection,
|
object $collection,
|
||||||
@@ -125,18 +139,18 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
}
|
}
|
||||||
|
|
||||||
$pieceId = (string) $owner->getId();
|
$pieceId = (string) $owner->getId();
|
||||||
if ($pieceId === '') {
|
if ('' === $pieceId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$mapping = $collection->getMapping();
|
$mapping = $collection->getMapping();
|
||||||
$fieldName = $mapping['fieldName'] ?? null;
|
$fieldName = $mapping['fieldName'] ?? null;
|
||||||
if ($fieldName !== 'constructeurs') {
|
if ('constructeurs' !== $fieldName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$before = $this->normalizeCollection($collection->getSnapshot());
|
$before = $this->normalizeCollection($collection->getSnapshot());
|
||||||
$after = $this->normalizeCollection($collection->toArray());
|
$after = $this->normalizeCollection($collection->toArray());
|
||||||
|
|
||||||
if ($before === $after) {
|
if ($before === $after) {
|
||||||
return;
|
return;
|
||||||
@@ -145,13 +159,82 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
$diff = [
|
$diff = [
|
||||||
'constructeurIds' => [
|
'constructeurIds' => [
|
||||||
'from' => $before,
|
'from' => $before,
|
||||||
'to' => $after,
|
'to' => $after,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$pendingUpdates[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff);
|
$pendingUpdates[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff);
|
||||||
$pendingSnapshots[$pieceId] = $this->snapshotPiece($owner);
|
$pendingSnapshots[$pieceId] = $this->snapshotPiece($owner);
|
||||||
$pendingPieces[$pieceId] = $owner;
|
$pendingPieces[$pieceId] = $owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
|
* @param array<string, Piece> $pendingPieces
|
||||||
|
*/
|
||||||
|
private function collectCustomFieldValueChanges(
|
||||||
|
UnitOfWork $uow,
|
||||||
|
array &$pendingUpdates,
|
||||||
|
array &$pendingSnapshots,
|
||||||
|
array &$pendingPieces,
|
||||||
|
): void {
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if ($entity instanceof CustomFieldValue) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingPieces);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof CustomFieldValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$changeSet = $uow->getEntityChangeSet($entity);
|
||||||
|
if (!isset($changeSet['value'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
[$oldVal, $newVal] = $changeSet['value'];
|
||||||
|
if ($oldVal !== $newVal) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, $oldVal, $newVal, $pendingUpdates, $pendingSnapshots, $pendingPieces);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if ($entity instanceof CustomFieldValue) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingPieces);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
|
* @param array<string, Piece> $pendingPieces
|
||||||
|
*/
|
||||||
|
private function trackCustomFieldValueChange(
|
||||||
|
CustomFieldValue $cfv,
|
||||||
|
mixed $from,
|
||||||
|
mixed $to,
|
||||||
|
array &$pendingUpdates,
|
||||||
|
array &$pendingSnapshots,
|
||||||
|
array &$pendingPieces,
|
||||||
|
): void {
|
||||||
|
$owner = $cfv->getPiece();
|
||||||
|
if (!$owner instanceof Piece) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ownerId = (string) $owner->getId();
|
||||||
|
if ('' === $ownerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldName = 'customField:'.$cfv->getCustomField()->getName();
|
||||||
|
$diff = [$fieldName => ['from' => $from, 'to' => $to]];
|
||||||
|
|
||||||
|
$pendingUpdates[$ownerId] = $this->mergeDiffs($pendingUpdates[$ownerId] ?? [], $diff);
|
||||||
|
$pendingSnapshots[$ownerId] = $this->snapshotPiece($owner);
|
||||||
|
$pendingPieces[$ownerId] = $owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||||
@@ -166,13 +249,14 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
||||||
|
*
|
||||||
* @return array<string, array{from:mixed, to:mixed}>
|
* @return array<string, array{from:mixed, to:mixed}>
|
||||||
*/
|
*/
|
||||||
private function buildDiffFromChangeSet(array $changeSet): array
|
private function buildDiffFromChangeSet(array $changeSet): array
|
||||||
{
|
{
|
||||||
$diff = [];
|
$diff = [];
|
||||||
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
||||||
if ($field === 'updatedAt' || $field === 'createdAt') {
|
if ('updatedAt' === $field || 'createdAt' === $field) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +269,7 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
$diff[$field] = [
|
$diff[$field] = [
|
||||||
'from' => $normalizedOld,
|
'from' => $normalizedOld,
|
||||||
'to' => $normalizedNew,
|
'to' => $normalizedNew,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,51 +279,57 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
private function snapshotPiece(Piece $piece): array
|
private function snapshotPiece(Piece $piece): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $piece->getId(),
|
'id' => $piece->getId(),
|
||||||
'name' => $piece->getName(),
|
'name' => $piece->getName(),
|
||||||
'reference' => $piece->getReference(),
|
'reference' => $piece->getReference(),
|
||||||
'prix' => $piece->getPrix(),
|
'prix' => $piece->getPrix(),
|
||||||
'typePiece' => $this->normalizeValue($piece->getTypePiece()),
|
'typePiece' => $this->normalizeValue($piece->getTypePiece()),
|
||||||
'product' => $this->normalizeValue($piece->getProduct()),
|
'product' => $this->normalizeValue($piece->getProduct()),
|
||||||
'productIds' => $piece->getProductIds(),
|
'productIds' => $piece->getProductIds(),
|
||||||
'constructeurIds' => $this->normalizeCollection($piece->getConstructeurs()),
|
'constructeurIds' => $this->normalizeCollection($piece->getConstructeurs()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param iterable<mixed> $items
|
* @param iterable<mixed> $items
|
||||||
* @return list<string>
|
*
|
||||||
|
* @return list<array{id: string, name: string}|string>
|
||||||
*/
|
*/
|
||||||
private function normalizeCollection(iterable $items): array
|
private function normalizeCollection(iterable $items): array
|
||||||
{
|
{
|
||||||
$ids = [];
|
$entries = [];
|
||||||
|
$seen = [];
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
if (\is_object($item) && \method_exists($item, 'getId')) {
|
if (is_object($item) && method_exists($item, 'getId')) {
|
||||||
$id = $item->getId();
|
$id = $item->getId();
|
||||||
if ($id !== null && $id !== '') {
|
if (null === $id || '' === $id || isset($seen[(string) $id])) {
|
||||||
$ids[] = (string) $id;
|
continue;
|
||||||
|
}
|
||||||
|
$seen[(string) $id] = true;
|
||||||
|
if (method_exists($item, 'getName')) {
|
||||||
|
$entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
|
||||||
|
} else {
|
||||||
|
$entries[] = (string) $id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort($ids);
|
return $entries;
|
||||||
|
|
||||||
return array_values(array_unique($ids));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeValue(mixed $value): mixed
|
private function normalizeValue(mixed $value): mixed
|
||||||
{
|
{
|
||||||
if ($value === null || \is_scalar($value)) {
|
if (null === $value || is_scalar($value)) {
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($value instanceof \DateTimeInterface) {
|
if ($value instanceof DateTimeInterface) {
|
||||||
return $value->format(\DateTimeInterface::ATOM);
|
return $value->format(DateTimeInterface::ATOM);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($value instanceof ModelType) {
|
if ($value instanceof ModelType) {
|
||||||
return [
|
return [
|
||||||
'id' => $value->getId(),
|
'id' => $value->getId(),
|
||||||
'name' => $value->getName(),
|
'name' => $value->getName(),
|
||||||
'code' => $value->getCode(),
|
'code' => $value->getCode(),
|
||||||
];
|
];
|
||||||
@@ -247,8 +337,8 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
if ($value instanceof Product) {
|
if ($value instanceof Product) {
|
||||||
return [
|
return [
|
||||||
'id' => $value->getId(),
|
'id' => $value->getId(),
|
||||||
'name' => $value->getName(),
|
'name' => $value->getName(),
|
||||||
'reference' => $value->getReference(),
|
'reference' => $value->getReference(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -257,11 +347,11 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
return $this->normalizeCollection($value);
|
return $this->normalizeCollection($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (\is_object($value) && \method_exists($value, 'getId')) {
|
if (is_object($value) && method_exists($value, 'getId')) {
|
||||||
return (string) $value->getId();
|
return (string) $value->getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (\is_array($value)) {
|
if (is_array($value)) {
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +361,7 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
/**
|
/**
|
||||||
* @param array<string, array{from:mixed, to:mixed}> $base
|
* @param array<string, array{from:mixed, to:mixed}> $base
|
||||||
* @param array<string, array{from:mixed, to:mixed}> $extra
|
* @param array<string, array{from:mixed, to:mixed}> $extra
|
||||||
|
*
|
||||||
* @return array<string, array{from:mixed, to:mixed}>
|
* @return array<string, array{from:mixed, to:mixed}>
|
||||||
*/
|
*/
|
||||||
private function mergeDiffs(array $base, array $extra): array
|
private function mergeDiffs(array $base, array $extra): array
|
||||||
@@ -284,17 +375,23 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
private function resolveActorProfileId(): ?string
|
private function resolveActorProfileId(): ?string
|
||||||
{
|
{
|
||||||
$session = $this->requestStack->getSession();
|
try {
|
||||||
if (!$session instanceof SessionInterface) {
|
$session = $this->requestStack->getSession();
|
||||||
return null;
|
if ($session instanceof SessionInterface) {
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if ($profileId) {
|
||||||
|
return (string) $profileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
// No session available (CLI context, etc.)
|
||||||
}
|
}
|
||||||
|
|
||||||
$profileId = $session->get('profileId');
|
$user = $this->security->getUser();
|
||||||
if (!$profileId) {
|
if ($user instanceof Profile) {
|
||||||
return null;
|
return $user->getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (string) $profileId;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ use Doctrine\ORM\Events;
|
|||||||
*/
|
*/
|
||||||
final class PieceProductSyncSubscriber implements EventSubscriber
|
final class PieceProductSyncSubscriber implements EventSubscriber
|
||||||
{
|
{
|
||||||
public function __construct(private readonly ProductRepository $productRepository)
|
public function __construct(private readonly ProductRepository $productRepository) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSubscribedEvents(): array
|
public function getSubscribedEvents(): array
|
||||||
{
|
{
|
||||||
@@ -47,7 +45,7 @@ final class PieceProductSyncSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
$this->syncPrimaryProduct($entity);
|
$this->syncPrimaryProduct($entity);
|
||||||
|
|
||||||
$em = $args->getObjectManager();
|
$em = $args->getObjectManager();
|
||||||
$meta = $em->getClassMetadata(Piece::class);
|
$meta = $em->getClassMetadata(Piece::class);
|
||||||
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $entity);
|
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $entity);
|
||||||
}
|
}
|
||||||
@@ -56,7 +54,7 @@ final class PieceProductSyncSubscriber implements EventSubscriber
|
|||||||
{
|
{
|
||||||
$productIds = $piece->getProductIds();
|
$productIds = $piece->getProductIds();
|
||||||
|
|
||||||
if ($productIds === []) {
|
if ([] === $productIds) {
|
||||||
// If no explicit list is provided, keep the legacy relation as-is.
|
// If no explicit list is provided, keep the legacy relation as-is.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -77,4 +75,3 @@ final class PieceProductSyncSubscriber implements EventSubscriber
|
|||||||
$piece->setProduct($primaryProduct);
|
$piece->setProduct($primaryProduct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ declare(strict_types=1);
|
|||||||
namespace App\EventSubscriber;
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
use App\Entity\AuditLog;
|
use App\Entity\AuditLog;
|
||||||
|
use App\Entity\CustomFieldValue;
|
||||||
use App\Entity\ModelType;
|
use App\Entity\ModelType;
|
||||||
use App\Entity\Product;
|
use App\Entity\Product;
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use DateTimeInterface;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\Common\EventSubscriber;
|
use Doctrine\Common\EventSubscriber;
|
||||||
@@ -14,8 +17,16 @@ use Doctrine\ORM\EntityManagerInterface;
|
|||||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||||
use Doctrine\ORM\Events;
|
use Doctrine\ORM\Events;
|
||||||
use Doctrine\ORM\PersistentCollection;
|
use Doctrine\ORM\PersistentCollection;
|
||||||
|
use Doctrine\ORM\UnitOfWork;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use function is_array;
|
||||||
|
use function is_object;
|
||||||
|
use function is_scalar;
|
||||||
|
use function method_exists;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record a lightweight, per-product audit trail.
|
* Record a lightweight, per-product audit trail.
|
||||||
@@ -27,9 +38,10 @@ use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
|||||||
#[AsDoctrineListener(event: Events::onFlush)]
|
#[AsDoctrineListener(event: Events::onFlush)]
|
||||||
final class ProductAuditSubscriber implements EventSubscriber
|
final class ProductAuditSubscriber implements EventSubscriber
|
||||||
{
|
{
|
||||||
public function __construct(private readonly RequestStack $requestStack)
|
public function __construct(
|
||||||
{
|
private readonly RequestStack $requestStack,
|
||||||
}
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function getSubscribedEvents(): array
|
public function getSubscribedEvents(): array
|
||||||
{
|
{
|
||||||
@@ -45,18 +57,18 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$uow = $em->getUnitOfWork();
|
$uow = $em->getUnitOfWork();
|
||||||
$actorProfileId = $this->resolveActorProfileId();
|
$actorProfileId = $this->resolveActorProfileId();
|
||||||
$pendingUpdates = [];
|
$pendingUpdates = [];
|
||||||
$pendingSnapshots = [];
|
$pendingSnapshots = [];
|
||||||
$pendingProducts = [];
|
$pendingProducts = [];
|
||||||
|
|
||||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
if (!$entity instanceof Product) {
|
if (!$entity instanceof Product) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
$snapshot = $this->snapshotProduct($entity);
|
$snapshot = $this->snapshotProduct($entity);
|
||||||
$this->persistAuditLog($em, new AuditLog('product', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
$this->persistAuditLog($em, new AuditLog('product', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||||
}
|
}
|
||||||
@@ -67,15 +79,15 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
}
|
}
|
||||||
|
|
||||||
$productId = (string) $entity->getId();
|
$productId = (string) $entity->getId();
|
||||||
if ($productId === '') {
|
if ('' === $productId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
if ($diff !== []) {
|
if ([] !== $diff) {
|
||||||
$pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff);
|
$pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff);
|
||||||
$pendingSnapshots[$productId] = $this->snapshotProduct($entity);
|
$pendingSnapshots[$productId] = $this->snapshotProduct($entity);
|
||||||
$pendingProducts[$productId] = $entity;
|
$pendingProducts[$productId] = $entity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,8 +108,10 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingProducts);
|
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingProducts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingProducts);
|
||||||
|
|
||||||
foreach ($pendingUpdates as $productId => $diff) {
|
foreach ($pendingUpdates as $productId => $diff) {
|
||||||
if ($diff === []) {
|
if ([] === $diff) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,8 +127,8 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
* @param array<string, Product> $pendingProducts
|
* @param array<string, Product> $pendingProducts
|
||||||
*/
|
*/
|
||||||
private function collectCollectionUpdate(
|
private function collectCollectionUpdate(
|
||||||
object $collection,
|
object $collection,
|
||||||
@@ -132,18 +146,18 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
}
|
}
|
||||||
|
|
||||||
$productId = (string) $owner->getId();
|
$productId = (string) $owner->getId();
|
||||||
if ($productId === '') {
|
if ('' === $productId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$mapping = $collection->getMapping();
|
$mapping = $collection->getMapping();
|
||||||
$fieldName = $mapping['fieldName'] ?? null;
|
$fieldName = $mapping['fieldName'] ?? null;
|
||||||
if ($fieldName !== 'constructeurs') {
|
if ('constructeurs' !== $fieldName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$before = $this->normalizeCollection($collection->getSnapshot());
|
$before = $this->normalizeCollection($collection->getSnapshot());
|
||||||
$after = $this->normalizeCollection($collection->toArray());
|
$after = $this->normalizeCollection($collection->toArray());
|
||||||
|
|
||||||
if ($before === $after) {
|
if ($before === $after) {
|
||||||
return;
|
return;
|
||||||
@@ -152,13 +166,82 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
$diff = [
|
$diff = [
|
||||||
'constructeurIds' => [
|
'constructeurIds' => [
|
||||||
'from' => $before,
|
'from' => $before,
|
||||||
'to' => $after,
|
'to' => $after,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff);
|
$pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff);
|
||||||
$pendingSnapshots[$productId] = $this->snapshotProduct($owner);
|
$pendingSnapshots[$productId] = $this->snapshotProduct($owner);
|
||||||
$pendingProducts[$productId] = $owner;
|
$pendingProducts[$productId] = $owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
|
* @param array<string, Product> $pendingProducts
|
||||||
|
*/
|
||||||
|
private function collectCustomFieldValueChanges(
|
||||||
|
UnitOfWork $uow,
|
||||||
|
array &$pendingUpdates,
|
||||||
|
array &$pendingSnapshots,
|
||||||
|
array &$pendingProducts,
|
||||||
|
): void {
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if ($entity instanceof CustomFieldValue) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingProducts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof CustomFieldValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$changeSet = $uow->getEntityChangeSet($entity);
|
||||||
|
if (!isset($changeSet['value'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
[$oldVal, $newVal] = $changeSet['value'];
|
||||||
|
if ($oldVal !== $newVal) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, $oldVal, $newVal, $pendingUpdates, $pendingSnapshots, $pendingProducts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if ($entity instanceof CustomFieldValue) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingProducts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
|
* @param array<string, Product> $pendingProducts
|
||||||
|
*/
|
||||||
|
private function trackCustomFieldValueChange(
|
||||||
|
CustomFieldValue $cfv,
|
||||||
|
mixed $from,
|
||||||
|
mixed $to,
|
||||||
|
array &$pendingUpdates,
|
||||||
|
array &$pendingSnapshots,
|
||||||
|
array &$pendingProducts,
|
||||||
|
): void {
|
||||||
|
$owner = $cfv->getProduct();
|
||||||
|
if (!$owner instanceof Product) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ownerId = (string) $owner->getId();
|
||||||
|
if ('' === $ownerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldName = 'customField:'.$cfv->getCustomField()->getName();
|
||||||
|
$diff = [$fieldName => ['from' => $from, 'to' => $to]];
|
||||||
|
|
||||||
|
$pendingUpdates[$ownerId] = $this->mergeDiffs($pendingUpdates[$ownerId] ?? [], $diff);
|
||||||
|
$pendingSnapshots[$ownerId] = $this->snapshotProduct($owner);
|
||||||
|
$pendingProducts[$ownerId] = $owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||||
@@ -174,6 +257,7 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
||||||
|
*
|
||||||
* @return array<string, array{from:mixed, to:mixed}>
|
* @return array<string, array{from:mixed, to:mixed}>
|
||||||
*/
|
*/
|
||||||
private function buildDiffFromChangeSet(array $changeSet): array
|
private function buildDiffFromChangeSet(array $changeSet): array
|
||||||
@@ -181,7 +265,7 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
$diff = [];
|
$diff = [];
|
||||||
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
||||||
// Skip noisy timestamps managed automatically.
|
// Skip noisy timestamps managed automatically.
|
||||||
if ($field === 'updatedAt' || $field === 'createdAt') {
|
if ('updatedAt' === $field || 'createdAt' === $field) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +278,7 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
$diff[$field] = [
|
$diff[$field] = [
|
||||||
'from' => $normalizedOld,
|
'from' => $normalizedOld,
|
||||||
'to' => $normalizedNew,
|
'to' => $normalizedNew,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,11 +288,11 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
private function snapshotProduct(Product $product): array
|
private function snapshotProduct(Product $product): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $product->getId(),
|
'id' => $product->getId(),
|
||||||
'name' => $product->getName(),
|
'name' => $product->getName(),
|
||||||
'reference' => $product->getReference(),
|
'reference' => $product->getReference(),
|
||||||
'supplierPrice' => $product->getSupplierPrice(),
|
'supplierPrice' => $product->getSupplierPrice(),
|
||||||
'typeProduct' => $this->normalizeValue($product->getTypeProduct()),
|
'typeProduct' => $this->normalizeValue($product->getTypeProduct()),
|
||||||
'constructeurIds' => $this->normalizeCollection($product->getConstructeurs()),
|
'constructeurIds' => $this->normalizeCollection($product->getConstructeurs()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -216,6 +300,7 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
/**
|
/**
|
||||||
* @param array<string, array{from:mixed, to:mixed}> $base
|
* @param array<string, array{from:mixed, to:mixed}> $base
|
||||||
* @param array<string, array{from:mixed, to:mixed}> $extra
|
* @param array<string, array{from:mixed, to:mixed}> $extra
|
||||||
|
*
|
||||||
* @return array<string, array{from:mixed, to:mixed}>
|
* @return array<string, array{from:mixed, to:mixed}>
|
||||||
*/
|
*/
|
||||||
private function mergeDiffs(array $base, array $extra): array
|
private function mergeDiffs(array $base, array $extra): array
|
||||||
@@ -229,38 +314,44 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param iterable<mixed> $items
|
* @param iterable<mixed> $items
|
||||||
* @return list<string>
|
*
|
||||||
|
* @return list<array{id: string, name: string}|string>
|
||||||
*/
|
*/
|
||||||
private function normalizeCollection(iterable $items): array
|
private function normalizeCollection(iterable $items): array
|
||||||
{
|
{
|
||||||
$ids = [];
|
$entries = [];
|
||||||
|
$seen = [];
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
if (\is_object($item) && \method_exists($item, 'getId')) {
|
if (is_object($item) && method_exists($item, 'getId')) {
|
||||||
$id = $item->getId();
|
$id = $item->getId();
|
||||||
if ($id !== null && $id !== '') {
|
if (null === $id || '' === $id || isset($seen[(string) $id])) {
|
||||||
$ids[] = (string) $id;
|
continue;
|
||||||
|
}
|
||||||
|
$seen[(string) $id] = true;
|
||||||
|
if (method_exists($item, 'getName')) {
|
||||||
|
$entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
|
||||||
|
} else {
|
||||||
|
$entries[] = (string) $id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort($ids);
|
return $entries;
|
||||||
|
|
||||||
return array_values(array_unique($ids));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeValue(mixed $value): mixed
|
private function normalizeValue(mixed $value): mixed
|
||||||
{
|
{
|
||||||
if ($value === null || \is_scalar($value)) {
|
if (null === $value || is_scalar($value)) {
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($value instanceof \DateTimeInterface) {
|
if ($value instanceof DateTimeInterface) {
|
||||||
return $value->format(\DateTimeInterface::ATOM);
|
return $value->format(DateTimeInterface::ATOM);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($value instanceof ModelType) {
|
if ($value instanceof ModelType) {
|
||||||
return [
|
return [
|
||||||
'id' => $value->getId(),
|
'id' => $value->getId(),
|
||||||
'name' => $value->getName(),
|
'name' => $value->getName(),
|
||||||
'code' => $value->getCode(),
|
'code' => $value->getCode(),
|
||||||
];
|
];
|
||||||
@@ -270,11 +361,11 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
return $this->normalizeCollection($value);
|
return $this->normalizeCollection($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (\is_object($value) && \method_exists($value, 'getId')) {
|
if (is_object($value) && method_exists($value, 'getId')) {
|
||||||
return (string) $value->getId();
|
return (string) $value->getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (\is_array($value)) {
|
if (is_array($value)) {
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,16 +374,23 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
|
|
||||||
private function resolveActorProfileId(): ?string
|
private function resolveActorProfileId(): ?string
|
||||||
{
|
{
|
||||||
$session = $this->requestStack->getSession();
|
try {
|
||||||
if (!$session instanceof SessionInterface) {
|
$session = $this->requestStack->getSession();
|
||||||
return null;
|
if ($session instanceof SessionInterface) {
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if ($profileId) {
|
||||||
|
return (string) $profileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
// No session available (CLI context, etc.)
|
||||||
}
|
}
|
||||||
|
|
||||||
$profileId = $session->get('profileId');
|
$user = $this->security->getUser();
|
||||||
if (!$profileId) {
|
if ($user instanceof Profile) {
|
||||||
return null;
|
return $user->getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (string) $profileId;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
|||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||||
use Symfony\Component\HttpKernel\KernelEvents;
|
use Symfony\Component\HttpKernel\KernelEvents;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
final class UniqueConstraintSubscriber implements EventSubscriberInterface
|
final class UniqueConstraintSubscriber implements EventSubscriberInterface
|
||||||
{
|
{
|
||||||
@@ -30,15 +31,15 @@ final class UniqueConstraintSubscriber implements EventSubscriberInterface
|
|||||||
$event->setResponse(new JsonResponse(
|
$event->setResponse(new JsonResponse(
|
||||||
[
|
[
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'error' => 'nom duplique',
|
'error' => 'nom duplique',
|
||||||
],
|
],
|
||||||
JsonResponse::HTTP_CONFLICT
|
JsonResponse::HTTP_CONFLICT
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function findUniqueConstraintViolation(\Throwable $throwable): ?UniqueConstraintViolationException
|
private function findUniqueConstraintViolation(Throwable $throwable): ?UniqueConstraintViolationException
|
||||||
{
|
{
|
||||||
for ($current = $throwable; $current !== null; $current = $current->getPrevious()) {
|
for ($current = $throwable; null !== $current; $current = $current->getPrevious()) {
|
||||||
if ($current instanceof UniqueConstraintViolationException) {
|
if ($current instanceof UniqueConstraintViolationException) {
|
||||||
return $current;
|
return $current;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,46 @@ final class AuditLogRepository extends ServiceEntityRepository
|
|||||||
->orderBy('a.createdAt', 'DESC')
|
->orderBy('a.createdAt', 'DESC')
|
||||||
->setMaxResults($limit)
|
->setMaxResults($limit)
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->getResult();
|
->getResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{entityType?: string, action?: string} $filters
|
||||||
|
*
|
||||||
|
* @return array{items: list<AuditLog>, total: int}
|
||||||
|
*/
|
||||||
|
public function findAllPaginated(int $page = 1, int $itemsPerPage = 30, array $filters = []): array
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('a')
|
||||||
|
->orderBy('a.createdAt', 'DESC')
|
||||||
|
;
|
||||||
|
|
||||||
|
if (!empty($filters['entityType'])) {
|
||||||
|
$qb->andWhere('a.entityType = :entityType')
|
||||||
|
->setParameter('entityType', $filters['entityType'])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['action'])) {
|
||||||
|
$qb->andWhere('a.action = :action')
|
||||||
|
->setParameter('action', $filters['action'])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
$countQb = clone $qb;
|
||||||
|
$countQb->select('COUNT(a.id)')
|
||||||
|
->resetDQLPart('orderBy')
|
||||||
|
;
|
||||||
|
$total = (int) $countQb->getQuery()->getSingleScalarResult();
|
||||||
|
|
||||||
|
$qb->setFirstResult(($page - 1) * $itemsPerPage)
|
||||||
|
->setMaxResults($itemsPerPage)
|
||||||
|
;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'items' => $qb->getQuery()->getResult(),
|
||||||
|
'total' => $total,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
src/Security/SessionProfileAuthenticator.php
Normal file
65
src/Security/SessionProfileAuthenticator.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Security;
|
||||||
|
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||||
|
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||||
|
|
||||||
|
final class SessionProfileAuthenticator extends AbstractAuthenticator
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function supports(Request $request): ?bool
|
||||||
|
{
|
||||||
|
if (!$request->hasSession()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $request->getSession()->has('profileId');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authenticate(Request $request): Passport
|
||||||
|
{
|
||||||
|
$profileId = $request->getSession()->get('profileId');
|
||||||
|
|
||||||
|
return new SelfValidatingPassport(
|
||||||
|
new UserBadge($profileId, function (string $id): Profile {
|
||||||
|
$profile = $this->profiles->find($id);
|
||||||
|
|
||||||
|
if (!$profile || !$profile->isActive()) {
|
||||||
|
throw new CustomUserMessageAuthenticationException('Profil introuvable ou inactif.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $profile;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||||
|
{
|
||||||
|
// Let the request continue normally
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||||
|
{
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => $exception->getMessageKey()],
|
||||||
|
JsonResponse::HTTP_UNAUTHORIZED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
494
src/Service/ModelTypeCategoryConversionService.php
Normal file
494
src/Service/ModelTypeCategoryConversionService.php
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use App\Enum\ModelCategory;
|
||||||
|
use App\Repository\ModelTypeRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class ModelTypeCategoryConversionService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Connection $connection,
|
||||||
|
private readonly ModelTypeRepository $modelTypes,
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{canConvert: bool, direction: null|string, itemCount: int, names: list<string>, blockers: list<string>}
|
||||||
|
*/
|
||||||
|
public function checkConversion(string $modelTypeId): array
|
||||||
|
{
|
||||||
|
$modelType = $this->modelTypes->find($modelTypeId);
|
||||||
|
|
||||||
|
if (!$modelType) {
|
||||||
|
return [
|
||||||
|
'canConvert' => false,
|
||||||
|
'direction' => null,
|
||||||
|
'itemCount' => 0,
|
||||||
|
'names' => [],
|
||||||
|
'blockers' => ['Catégorie introuvable.'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = $modelType->getCategory();
|
||||||
|
|
||||||
|
if (ModelCategory::PRODUCT === $category) {
|
||||||
|
return [
|
||||||
|
'canConvert' => false,
|
||||||
|
'direction' => null,
|
||||||
|
'itemCount' => 0,
|
||||||
|
'names' => [],
|
||||||
|
'blockers' => ['La conversion n\'est pas disponible pour les catégories de produit.'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ModelCategory::PIECE === $category) {
|
||||||
|
return $this->checkPieceToComponent($modelTypeId, $modelType->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->checkComponentToPiece($modelTypeId, $modelType->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{success: bool, convertedCount: int, error: null|string}
|
||||||
|
*/
|
||||||
|
public function convert(string $modelTypeId): array
|
||||||
|
{
|
||||||
|
$check = $this->checkConversion($modelTypeId);
|
||||||
|
|
||||||
|
if (!$check['canConvert']) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'convertedCount' => 0,
|
||||||
|
'error' => implode(' ', $check['blockers']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$modelType = $this->modelTypes->find($modelTypeId);
|
||||||
|
|
||||||
|
if (!$modelType) {
|
||||||
|
return ['success' => false, 'convertedCount' => 0, 'error' => 'Catégorie introuvable.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = $modelType->getCategory();
|
||||||
|
|
||||||
|
$direction = ModelCategory::PIECE === $category ? 'piece_to_component' : 'component_to_piece';
|
||||||
|
$names = $check['names'];
|
||||||
|
$modelName = $modelType->getName();
|
||||||
|
$modelCode = $modelType->getCode();
|
||||||
|
|
||||||
|
$this->connection->beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (ModelCategory::PIECE === $category) {
|
||||||
|
$count = $this->convertPieceToComponent($modelTypeId);
|
||||||
|
} else {
|
||||||
|
$count = $this->convertComponentToPiece($modelTypeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logConversionAudit($modelTypeId, $modelName, $modelCode, $direction, $count, $names);
|
||||||
|
|
||||||
|
$this->connection->commit();
|
||||||
|
|
||||||
|
return ['success' => true, 'convertedCount' => $count, 'error' => null];
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->connection->rollBack();
|
||||||
|
|
||||||
|
return ['success' => false, 'convertedCount' => 0, 'error' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{canConvert: bool, direction: string, itemCount: int, names: list<string>, blockers: list<string>}
|
||||||
|
*/
|
||||||
|
private function checkPieceToComponent(string $modelTypeId, string $modelTypeName): array
|
||||||
|
{
|
||||||
|
$blockers = [];
|
||||||
|
|
||||||
|
$pieceCount = (int) $this->connection->fetchOne(
|
||||||
|
'SELECT COUNT(*) FROM pieces WHERE typepieceid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
$names = $this->connection->fetchFirstColumn(
|
||||||
|
'SELECT name FROM pieces WHERE typepieceid = :id ORDER BY name',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check machine links
|
||||||
|
$machineLinked = (int) $this->connection->fetchOne(
|
||||||
|
'SELECT COUNT(*) FROM machine_piece_links mpl
|
||||||
|
JOIN pieces p ON mpl.pieceid = p.id
|
||||||
|
WHERE p.typepieceid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($machineLinked > 0) {
|
||||||
|
$blockers[] = sprintf('%d pièce(s) liée(s) à des machines.', $machineLinked);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check type machine requirements
|
||||||
|
$requirementCount = (int) $this->connection->fetchOne(
|
||||||
|
'SELECT COUNT(*) FROM type_machine_piece_requirements WHERE typepieceid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($requirementCount > 0) {
|
||||||
|
$blockers[] = sprintf('Utilisé dans %d modèle(s) de type de machine.', $requirementCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check name collision with existing composants
|
||||||
|
$collisions = $this->connection->fetchFirstColumn(
|
||||||
|
'SELECT p.name FROM pieces p
|
||||||
|
WHERE p.typepieceid = :id
|
||||||
|
AND p.name IN (SELECT c.name FROM composants c)',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ([] !== $collisions) {
|
||||||
|
$blockers[] = sprintf(
|
||||||
|
'Collision de nom avec des composants existants : %s.',
|
||||||
|
implode(', ', $collisions),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ModelType name collision
|
||||||
|
$nameCollision = (int) $this->connection->fetchOne(
|
||||||
|
'SELECT COUNT(*) FROM model_types WHERE category = :cat AND name = :name AND id != :id',
|
||||||
|
['cat' => ModelCategory::COMPONENT->value, 'name' => $modelTypeName, 'id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($nameCollision > 0) {
|
||||||
|
$blockers[] = sprintf('Une catégorie de composant « %s » existe déjà.', $modelTypeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'canConvert' => [] === $blockers,
|
||||||
|
'direction' => 'piece_to_component',
|
||||||
|
'itemCount' => $pieceCount,
|
||||||
|
'names' => $names,
|
||||||
|
'blockers' => $blockers,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{canConvert: bool, direction: string, itemCount: int, names: list<string>, blockers: list<string>}
|
||||||
|
*/
|
||||||
|
private function checkComponentToPiece(string $modelTypeId, string $modelTypeName): array
|
||||||
|
{
|
||||||
|
$blockers = [];
|
||||||
|
|
||||||
|
$composantCount = (int) $this->connection->fetchOne(
|
||||||
|
'SELECT COUNT(*) FROM composants WHERE typecomposantid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
$names = $this->connection->fetchFirstColumn(
|
||||||
|
'SELECT name FROM composants WHERE typecomposantid = :id ORDER BY name',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check machine links
|
||||||
|
$machineLinked = (int) $this->connection->fetchOne(
|
||||||
|
'SELECT COUNT(*) FROM machine_component_links mcl
|
||||||
|
JOIN composants c ON mcl.composantid = c.id
|
||||||
|
WHERE c.typecomposantid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($machineLinked > 0) {
|
||||||
|
$blockers[] = sprintf('%d composant(s) lié(s) à des machines.', $machineLinked);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check type machine requirements
|
||||||
|
$requirementCount = (int) $this->connection->fetchOne(
|
||||||
|
'SELECT COUNT(*) FROM type_machine_component_requirements WHERE typecomposantid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($requirementCount > 0) {
|
||||||
|
$blockers[] = sprintf('Utilisé dans %d modèle(s) de type de machine.', $requirementCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any composant has pieces or sub-components in structure
|
||||||
|
$withStructure = $this->connection->fetchAllAssociative(
|
||||||
|
'SELECT name, structure FROM composants WHERE typecomposantid = :id AND structure IS NOT NULL',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($withStructure as $row) {
|
||||||
|
$structure = json_decode($row['structure'], true);
|
||||||
|
|
||||||
|
if (!is_array($structure)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasPieces = !empty($structure['pieces']);
|
||||||
|
$hasSubcomponents = !empty($structure['subcomponents']);
|
||||||
|
|
||||||
|
if ($hasPieces || $hasSubcomponents) {
|
||||||
|
$parts = [];
|
||||||
|
|
||||||
|
if ($hasPieces) {
|
||||||
|
$parts[] = 'pièces';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasSubcomponents) {
|
||||||
|
$parts[] = 'sous-composants';
|
||||||
|
}
|
||||||
|
|
||||||
|
$blockers[] = sprintf(
|
||||||
|
'Le composant « %s » contient des %s dans sa structure.',
|
||||||
|
$row['name'],
|
||||||
|
implode(' et ', $parts),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check name collision with existing pieces
|
||||||
|
$collisions = $this->connection->fetchFirstColumn(
|
||||||
|
'SELECT c.name FROM composants c
|
||||||
|
WHERE c.typecomposantid = :id
|
||||||
|
AND c.name IN (SELECT p.name FROM pieces p)',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ([] !== $collisions) {
|
||||||
|
$blockers[] = sprintf(
|
||||||
|
'Collision de nom avec des pièces existantes : %s.',
|
||||||
|
implode(', ', $collisions),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ModelType name collision
|
||||||
|
$nameCollision = (int) $this->connection->fetchOne(
|
||||||
|
'SELECT COUNT(*) FROM model_types WHERE category = :cat AND name = :name AND id != :id',
|
||||||
|
['cat' => ModelCategory::PIECE->value, 'name' => $modelTypeName, 'id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($nameCollision > 0) {
|
||||||
|
$blockers[] = sprintf('Une catégorie de pièce « %s » existe déjà.', $modelTypeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'canConvert' => [] === $blockers,
|
||||||
|
'direction' => 'component_to_piece',
|
||||||
|
'itemCount' => $composantCount,
|
||||||
|
'names' => $names,
|
||||||
|
'blockers' => $blockers,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function convertPieceToComponent(string $modelTypeId): int
|
||||||
|
{
|
||||||
|
// 1. Insert into composants from pieces
|
||||||
|
$count = $this->connection->executeStatement(
|
||||||
|
'INSERT INTO composants (id, name, reference, prix, structure, typecomposantid, productid, createdat, updatedat)
|
||||||
|
SELECT id, name, reference, prix, NULL, typepieceid, productid, createdat, updatedat
|
||||||
|
FROM pieces
|
||||||
|
WHERE typepieceid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Transfer constructeur associations
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'INSERT INTO _composantconstructeurs (a, b)
|
||||||
|
SELECT pc.a, pc.b FROM _piececonstructeurs pc
|
||||||
|
WHERE pc.a IN (SELECT id FROM composants WHERE typecomposantid = :id)',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'DELETE FROM _piececonstructeurs
|
||||||
|
WHERE a IN (SELECT id FROM pieces WHERE typepieceid = :id)',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Transfer document references
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'UPDATE documents SET composantid = pieceid, pieceid = NULL
|
||||||
|
WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Transfer custom_field_values references
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'UPDATE custom_field_values SET composantid = pieceid, pieceid = NULL
|
||||||
|
WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Transfer custom_fields from typePiece to typeComposant
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'UPDATE custom_fields SET typecomposantid = typepieceid, typepieceid = NULL
|
||||||
|
WHERE typepieceid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. Delete original pieces
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'DELETE FROM pieces WHERE typepieceid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 7. Update ModelType
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'UPDATE model_types
|
||||||
|
SET category = :cat,
|
||||||
|
componentskeleton = pieceskeleton,
|
||||||
|
pieceskeleton = NULL,
|
||||||
|
updatedat = :now
|
||||||
|
WHERE id = :id',
|
||||||
|
[
|
||||||
|
'cat' => ModelCategory::COMPONENT->value,
|
||||||
|
'now' => new DateTimeImmutable()->format('Y-m-d H:i:s'),
|
||||||
|
'id' => $modelTypeId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function convertComponentToPiece(string $modelTypeId): int
|
||||||
|
{
|
||||||
|
// 1. Insert into pieces from composants
|
||||||
|
$count = $this->connection->executeStatement(
|
||||||
|
'INSERT INTO pieces (id, name, reference, prix, productids, typepieceid, productid, createdat, updatedat)
|
||||||
|
SELECT id, name, reference, prix, NULL, typecomposantid, productid, createdat, updatedat
|
||||||
|
FROM composants
|
||||||
|
WHERE typecomposantid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Transfer constructeur associations
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'INSERT INTO _piececonstructeurs (a, b)
|
||||||
|
SELECT cc.a, cc.b FROM _composantconstructeurs cc
|
||||||
|
WHERE cc.a IN (SELECT id FROM pieces WHERE typepieceid = :id)',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'DELETE FROM _composantconstructeurs
|
||||||
|
WHERE a IN (SELECT id FROM composants WHERE typecomposantid = :id)',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Transfer document references
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'UPDATE documents SET pieceid = composantid, composantid = NULL
|
||||||
|
WHERE composantid IN (SELECT id FROM composants WHERE typecomposantid = :id)',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Transfer custom_field_values references
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'UPDATE custom_field_values SET pieceid = composantid, composantid = NULL
|
||||||
|
WHERE composantid IN (SELECT id FROM composants WHERE typecomposantid = :id)',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Transfer custom_fields from typeComposant to typePiece
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'UPDATE custom_fields SET typepieceid = typecomposantid, typecomposantid = NULL
|
||||||
|
WHERE typecomposantid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. Delete original composants
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'DELETE FROM composants WHERE typecomposantid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 7. Update ModelType
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'UPDATE model_types
|
||||||
|
SET category = :cat,
|
||||||
|
pieceskeleton = componentskeleton,
|
||||||
|
componentskeleton = NULL,
|
||||||
|
updatedat = :now
|
||||||
|
WHERE id = :id',
|
||||||
|
[
|
||||||
|
'cat' => ModelCategory::PIECE->value,
|
||||||
|
'now' => new DateTimeImmutable()->format('Y-m-d H:i:s'),
|
||||||
|
'id' => $modelTypeId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $names
|
||||||
|
*/
|
||||||
|
private function logConversionAudit(
|
||||||
|
string $modelTypeId,
|
||||||
|
string $modelName,
|
||||||
|
string $modelCode,
|
||||||
|
string $direction,
|
||||||
|
int $convertedCount,
|
||||||
|
array $names,
|
||||||
|
): void {
|
||||||
|
$now = new DateTimeImmutable()->format('Y-m-d H:i:s');
|
||||||
|
$id = 'cl'.bin2hex(random_bytes(12));
|
||||||
|
|
||||||
|
$snapshot = [
|
||||||
|
'id' => $modelTypeId,
|
||||||
|
'name' => $modelName,
|
||||||
|
'code' => $modelCode,
|
||||||
|
];
|
||||||
|
|
||||||
|
$diff = [
|
||||||
|
'direction' => ['from' => null, 'to' => $direction],
|
||||||
|
'convertedCount' => ['from' => null, 'to' => $convertedCount],
|
||||||
|
'convertedNames' => ['from' => null, 'to' => $names],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'INSERT INTO audit_logs (id, entitytype, entityid, action, diff, snapshot, actorprofileid, createdat)
|
||||||
|
VALUES (:id, :entityType, :entityId, :action, :diff, :snapshot, :actor, :now)',
|
||||||
|
[
|
||||||
|
'id' => $id,
|
||||||
|
'entityType' => 'model_type',
|
||||||
|
'entityId' => $modelTypeId,
|
||||||
|
'action' => 'convert',
|
||||||
|
'diff' => json_encode($diff),
|
||||||
|
'snapshot' => json_encode($snapshot),
|
||||||
|
'actor' => $this->resolveActorProfileId(),
|
||||||
|
'now' => $now,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveActorProfileId(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$session = $this->requestStack->getSession();
|
||||||
|
if ($session instanceof SessionInterface) {
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if ($profileId) {
|
||||||
|
return (string) $profileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if ($user instanceof Profile) {
|
||||||
|
return $user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/State/ProfilePasswordHasher.php
Normal file
36
src/State/ProfilePasswordHasher.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProcessorInterface;
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
|
||||||
|
final class ProfilePasswordHasher implements ProcessorInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
|
||||||
|
private readonly ProcessorInterface $decorated,
|
||||||
|
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $uriVariables
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
*/
|
||||||
|
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||||
|
{
|
||||||
|
if ($data instanceof Profile && $data->getPlainPassword()) {
|
||||||
|
$data->setPassword(
|
||||||
|
$this->passwordHasher->hashPassword($data, $data->getPlainPassword())
|
||||||
|
);
|
||||||
|
$data->eraseCredentials();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->decorated->process($data, $operation, $uriVariables, $context);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user