Compare commits

..

4 Commits

Author SHA1 Message Date
gitea-actions
28394ce1b4 chore : bump version to v1.9.28
All checks were successful
Auto Tag Develop / tag (push) Successful in 7s
Build & Push Docker Image / build (push) Successful in 3m16s
2026-04-10 14:57:59 +00:00
Matthieu
8cfcb41a39 feat(conversion) : commande CLI pour convertir la catégorie Moteur de PIECE vers COMPONENT
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Migre les 18 pièces en composants, transfère documents, custom fields,
slots et skeleton requirements dans une transaction. Supporte --dry-run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:57:46 +02:00
gitea-actions
980a7c310e chore : bump version to v1.9.27
All checks were successful
Auto Tag Develop / tag (push) Successful in 9s
Build & Push Docker Image / build (push) Successful in 2m32s
2026-04-09 12:34:46 +00:00
Matthieu
00f18d1c7d feat(infra) : add monolog logging and persist logs in prod
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Install symfony/monolog-bundle with rotating_file handlers.
Add named volume inventory_logs for prod log persistence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:33:42 +02:00
10 changed files with 682 additions and 2 deletions

View File

@@ -7,6 +7,13 @@
"X-Profile-Id": "admin-default-profile",
"X-Profile-Password": "A123"
}
},
"lesstime": {
"type": "http",
"url": "http://project.malio-dev.fr/_mcp",
"headers": {
"Authorization": "Bearer b355c6cbf27d2a86d7eba1c3132c99bb3133f94cfd9e9243ffcc3c5ae1dc82c8"
}
}
}
}

View File

@@ -24,6 +24,7 @@
"symfony/framework-bundle": "8.0.*",
"symfony/mcp-bundle": "^0.6.0",
"symfony/mime": "8.0.*",
"symfony/monolog-bundle": "^4.0.2",
"symfony/property-access": "8.0.*",
"symfony/property-info": "8.0.*",
"symfony/rate-limiter": "8.0.*",

261
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f94dc3c05e9ba6be99c510aad3d17182",
"content-hash": "5c54b1589d9e815f4c9b7e5e1d2d69c7",
"packages": [
{
"name": "api-platform/doctrine-common",
@@ -2437,6 +2437,109 @@
},
"time": "2026-02-23T21:42:54+00:00"
},
{
"name": "monolog/monolog",
"version": "3.10.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0",
"reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/log": "^2.0 || ^3.0"
},
"provide": {
"psr/log-implementation": "3.0.0"
},
"require-dev": {
"aws/aws-sdk-php": "^3.0",
"doctrine/couchdb": "~1.0@dev",
"elasticsearch/elasticsearch": "^7 || ^8",
"ext-json": "*",
"graylog2/gelf-php": "^1.4.2 || ^2.0",
"guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^2.2",
"mongodb/mongodb": "^1.8 || ^2.0",
"php-amqplib/php-amqplib": "~2.4 || ^3",
"php-console/php-console": "^3.1.8",
"phpstan/phpstan": "^2",
"phpstan/phpstan-deprecation-rules": "^2",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "^10.5.17 || ^11.0.7",
"predis/predis": "^1.1 || ^2",
"rollbar/rollbar": "^4.0",
"ruflin/elastica": "^7 || ^8",
"symfony/mailer": "^5.4 || ^6",
"symfony/mime": "^5.4 || ^6"
},
"suggest": {
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
"elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
"ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
"ext-mbstring": "Allow to work properly with unicode symbols",
"ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
"ext-openssl": "Required to send log messages using SSL",
"ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
"mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
"rollbar/rollbar": "Allow sending log messages to Rollbar",
"ruflin/elastica": "Allow sending log messages to an Elastic Search server"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Monolog\\": "src/Monolog"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "https://seld.be"
}
],
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
"homepage": "https://github.com/Seldaek/monolog",
"keywords": [
"log",
"logging",
"psr-3"
],
"support": {
"issues": "https://github.com/Seldaek/monolog/issues",
"source": "https://github.com/Seldaek/monolog/tree/3.10.0"
},
"funding": [
{
"url": "https://github.com/Seldaek",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
"type": "tidelift"
}
],
"time": "2026-01-02T08:56:05+00:00"
},
{
"name": "nelmio/cors-bundle",
"version": "2.6.0",
@@ -5427,6 +5530,162 @@
],
"time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/monolog-bridge",
"version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/monolog-bridge.git",
"reference": "c6efdcbd5cc17cf7618fb4447053b792df6ae724"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/c6efdcbd5cc17cf7618fb4447053b792df6ae724",
"reference": "c6efdcbd5cc17cf7618fb4447053b792df6ae724",
"shasum": ""
},
"require": {
"monolog/monolog": "^3",
"php": ">=8.4",
"symfony/http-kernel": "^7.4|^8.0",
"symfony/service-contracts": "^2.5|^3"
},
"require-dev": {
"symfony/console": "^7.4|^8.0",
"symfony/http-client": "^7.4|^8.0",
"symfony/mailer": "^7.4|^8.0",
"symfony/messenger": "^7.4|^8.0",
"symfony/mime": "^7.4|^8.0",
"symfony/security-core": "^7.4|^8.0",
"symfony/var-dumper": "^7.4|^8.0"
},
"type": "symfony-bridge",
"autoload": {
"psr-4": {
"Symfony\\Bridge\\Monolog\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides integration for Monolog with various Symfony components",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/monolog-bridge/tree/v8.0.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/monolog-bundle",
"version": "v4.0.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/monolog-bundle.git",
"reference": "c012c6aba13129eb02aa7dd61e66e720911d8598"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/c012c6aba13129eb02aa7dd61e66e720911d8598",
"reference": "c012c6aba13129eb02aa7dd61e66e720911d8598",
"shasum": ""
},
"require": {
"composer-runtime-api": "^2.0",
"monolog/monolog": "^3.5",
"php": ">=8.2",
"symfony/config": "^7.3 || ^8.0",
"symfony/dependency-injection": "^7.3 || ^8.0",
"symfony/http-kernel": "^7.3 || ^8.0",
"symfony/monolog-bridge": "^7.3 || ^8.0",
"symfony/polyfill-php84": "^1.30"
},
"require-dev": {
"phpunit/phpunit": "^11.5.41 || ^12.3",
"symfony/console": "^7.3 || ^8.0",
"symfony/yaml": "^7.3 || ^8.0"
},
"type": "symfony-bundle",
"autoload": {
"psr-4": {
"Symfony\\Bundle\\MonologBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony MonologBundle",
"homepage": "https://symfony.com",
"keywords": [
"log",
"logging"
],
"support": {
"issues": "https://github.com/symfony/monolog-bundle/issues",
"source": "https://github.com/symfony/monolog-bundle/tree/v4.0.2"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-04-02T18:27:21+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v8.0.0",

View File

@@ -9,6 +9,7 @@ use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
use Nelmio\CorsBundle\NelmioCorsBundle;
use Symfony\AI\McpBundle\McpBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\MonologBundle\MonologBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
@@ -22,4 +23,5 @@ return [
ApiPlatformBundle::class => ['all' => true],
DAMADoctrineTestBundle::class => ['test' => true],
McpBundle::class => ['all' => true],
MonologBundle::class => ['all' => true],
];

View File

@@ -0,0 +1,56 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev:
monolog:
handlers:
main:
type: rotating_file
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
max_files: 7
channels: ["!event"]
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
when@test:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
when@prod:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!deprecation"]
buffer_size: 50
nested:
type: rotating_file
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
max_files: 30
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
deprecation:
type: rotating_file
channels: [deprecation]
path: "%kernel.logs_dir%/deprecations.log"
max_files: 7

View File

@@ -1,2 +1,2 @@
parameters:
app.version: '1.9.26'
app.version: '1.9.28'

View File

@@ -98,6 +98,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2092,6 +2093,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -4112,6 +4114,7 @@
"integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -4181,6 +4184,7 @@
"integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.44.1",
"@typescript-eslint/types": "8.44.1",
@@ -4977,6 +4981,7 @@
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
"integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/parser": "^7.28.4",
"@vue/compiler-core": "3.5.22",
@@ -5207,6 +5212,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5637,6 +5643,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -7069,6 +7076,7 @@
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -10490,6 +10498,7 @@
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
@@ -10536,6 +10545,7 @@
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.87.0.tgz",
"integrity": "sha512-uc47XrtHwkBoES4HFgwgfH9sqwAtJXgAIBq4fFBMZ4hWmgVZoExyn+L4g4VuaecVKXkz1bvlaHcfwHAJPQb5Gw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@oxc-project/types": "^0.87.0"
},
@@ -10937,6 +10947,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -11376,6 +11387,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"license": "MIT",
"peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -12118,6 +12130,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz",
"integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -13180,6 +13193,7 @@
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -13537,6 +13551,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@@ -13783,6 +13798,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -14186,6 +14202,7 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.22",
"@vue/compiler-sfc": "3.5.22",
@@ -14230,6 +14247,7 @@
"integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"debug": "^4.4.0",
"eslint-scope": "^8.2.0",
@@ -14253,6 +14271,7 @@
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},

View File

@@ -7,6 +7,10 @@ services:
- "8082:80"
volumes:
- ./storage:/var/www/html/var/storage/documents
- inventory_logs:/var/www/html/var/log
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
volumes:
inventory_logs:

View File

@@ -0,0 +1,320 @@
<?php
declare(strict_types=1);
namespace App\Command;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Throwable;
#[AsCommand(
name: 'app:convert-moteur-piece-to-component',
description: 'Convertit la catégorie "Moteur" (PIECE) en COMPONENT et migre toutes les pièces liées en composants.',
)]
class ConvertMoteurPieceToComponentCommand extends Command
{
private const MODEL_TYPE_ID = 'cmgytewe0002447ffup09bscr';
public function __construct(
private readonly Connection $connection,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Affiche les actions sans les exécuter');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$dryRun = $input->getOption('dry-run');
$io->title('Conversion catégorie "Moteur" : PIECE → COMPONENT');
// ── 1. Vérifications ──────────────────────────────────────────────
$modelType = $this->connection->fetchAssociative(
'SELECT id, name, code, category FROM model_types WHERE id = :id',
['id' => self::MODEL_TYPE_ID],
);
if (!$modelType) {
$io->error('ModelType "Moteur" introuvable (id: '.self::MODEL_TYPE_ID.')');
return Command::FAILURE;
}
if ('PIECE' !== $modelType['category']) {
$io->error(sprintf('Le ModelType "Moteur" est déjà de catégorie %s — rien à faire.', $modelType['category']));
return Command::FAILURE;
}
$pieces = $this->connection->fetchAllAssociative(
'SELECT id, name, reference FROM pieces WHERE typepieceid = :id ORDER BY name',
['id' => self::MODEL_TYPE_ID],
);
$pieceCount = count($pieces);
$io->info(sprintf('Pièces à convertir : %d', $pieceCount));
if ($pieceCount > 0) {
$io->table(
['ID', 'Nom', 'Référence'],
array_map(fn (array $p) => [$p['id'], $p['name'], $p['reference'] ?? '—'], $pieces),
);
}
// Check blockers
$blockers = [];
$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' => self::MODEL_TYPE_ID],
);
if ($machineLinked > 0) {
$blockers[] = sprintf('%d pièce(s) liée(s) à des machines — conversion impossible.', $machineLinked);
}
$nameCollisions = $this->connection->fetchFirstColumn(
'SELECT p.name FROM pieces p
WHERE p.typepieceid = :id
AND p.name IN (SELECT c.name FROM composants c)',
['id' => self::MODEL_TYPE_ID],
);
if ([] !== $nameCollisions) {
$blockers[] = sprintf('Collision de noms avec des composants existants : %s', implode(', ', $nameCollisions));
}
$categoryCollision = (int) $this->connection->fetchOne(
"SELECT COUNT(*) FROM model_types WHERE category = 'COMPONENT' AND name = :name AND id != :id",
['name' => $modelType['name'], 'id' => self::MODEL_TYPE_ID],
);
if ($categoryCollision > 0) {
$blockers[] = sprintf('Un ModelType composant « %s » existe déjà.', $modelType['name']);
}
if ([] !== $blockers) {
$io->error($blockers);
return Command::FAILURE;
}
// Summary of related data
$relatedCounts = $this->countRelatedData();
$io->section('Données liées à migrer');
$io->table(
['Table', 'Nombre'],
array_map(fn (string $k, int $v) => [$k, $v], array_keys($relatedCounts), array_values($relatedCounts)),
);
if ($dryRun) {
$io->warning('Mode dry-run : aucune modification effectuée.');
return Command::SUCCESS;
}
// ── 2. Exécution ──────────────────────────────────────────────────
$this->connection->beginTransaction();
try {
$now = new DateTimeImmutable()->format('Y-m-d H:i:s');
// 2a. Copier les pièces dans composants
$converted = $this->connection->executeStatement(
'INSERT INTO composants (id, name, reference, referenceauto, description, prix, typecomposantid, productid, version, createdat, updatedat)
SELECT id, name, reference, referenceauto, description, prix, typepieceid, productid, version, createdat, :now
FROM pieces
WHERE typepieceid = :id',
['id' => self::MODEL_TYPE_ID, 'now' => $now],
);
$io->text(sprintf('✓ %d pièce(s) copiée(s) dans composants', $converted));
// 2b. Transférer les documents
$docs = $this->connection->executeStatement(
'UPDATE documents SET composantid = pieceid, pieceid = NULL
WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)',
['id' => self::MODEL_TYPE_ID],
);
$io->text(sprintf('✓ %d document(s) transféré(s)', $docs));
// 2c. Transférer les custom_field_values
$cfv = $this->connection->executeStatement(
'UPDATE custom_field_values SET composantid = pieceid, pieceid = NULL
WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)',
['id' => self::MODEL_TYPE_ID],
);
$io->text(sprintf('✓ %d valeur(s) de champs perso transférée(s)', $cfv));
// 2d. Transférer les custom_fields (définitions)
$cf = $this->connection->executeStatement(
'UPDATE custom_fields SET typecomposantid = typepieceid, typepieceid = NULL
WHERE typepieceid = :id',
['id' => self::MODEL_TYPE_ID],
);
$io->text(sprintf('✓ %d définition(s) de champs perso transférée(s)', $cf));
// 2e. Transférer les constructeur links
$ctorLinks = $this->connection->executeStatement(
"INSERT INTO composant_constructeur_links (id, composantid, constructeurid, supplierreference, createdat, updatedat)
SELECT 'cl' || substring(md5(random()::text || clock_timestamp()::text), 1, 24),
pcl.pieceid, pcl.constructeurid, pcl.supplierreference, pcl.createdat, :now
FROM piece_constructeur_links pcl
WHERE pcl.pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)",
['id' => self::MODEL_TYPE_ID, 'now' => $now],
);
if ($ctorLinks > 0) {
$this->connection->executeStatement(
'DELETE FROM piece_constructeur_links
WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)',
['id' => self::MODEL_TYPE_ID],
);
}
$io->text(sprintf('✓ %d lien(s) constructeur transféré(s)', $ctorLinks));
// 2f. Convertir composant_piece_slots → composant_subcomponent_slots
$slots = $this->connection->executeStatement(
"INSERT INTO composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat)
SELECT 'cl' || substring(md5(random()::text || clock_timestamp()::text), 1, 24),
cps.composantid,
COALESCE(sp.name, 'Moteur'),
'moteur',
cps.typepieceid,
cps.selectedpieceid,
cps.position,
cps.createdat,
:now
FROM composant_piece_slots cps
LEFT JOIN pieces sp ON sp.id = cps.selectedpieceid
WHERE cps.typepieceid = :id",
['id' => self::MODEL_TYPE_ID, 'now' => $now],
);
$this->connection->executeStatement(
'DELETE FROM composant_piece_slots WHERE typepieceid = :id',
['id' => self::MODEL_TYPE_ID],
);
$io->text(sprintf('✓ %d slot(s) pièce convertis en slots sous-composant', $slots));
// 2g. Convertir skeleton_piece_requirements → skeleton_subcomponent_requirements
$skelReqs = $this->connection->executeStatement(
"INSERT INTO skeleton_subcomponent_requirements (id, modeltypeid, alias, familycode, typecomposantid, position, createdat, updatedat)
SELECT 'cl' || substring(md5(random()::text || clock_timestamp()::text), 1, 24),
spr.modeltypeid,
'Moteur',
'moteur',
spr.typepieceid,
spr.position,
spr.createdat,
:now
FROM skeleton_piece_requirements spr
WHERE spr.typepieceid = :id",
['id' => self::MODEL_TYPE_ID, 'now' => $now],
);
$this->connection->executeStatement(
'DELETE FROM skeleton_piece_requirements WHERE typepieceid = :id',
['id' => self::MODEL_TYPE_ID],
);
$io->text(sprintf('✓ %d skeleton requirement(s) convertis', $skelReqs));
// 2h. Mettre à jour audit_logs entity_type
$auditUpdated = $this->connection->executeStatement(
"UPDATE audit_logs SET entitytype = 'composant'
WHERE entitytype = 'piece'
AND entityid IN (SELECT id FROM pieces WHERE typepieceid = :id)",
['id' => self::MODEL_TYPE_ID],
);
$io->text(sprintf('✓ %d audit log(s) mis à jour', $auditUpdated));
// 2i. Mettre à jour comments entity_type
$commentsUpdated = $this->connection->executeStatement(
"UPDATE comments SET entity_type = 'composant'
WHERE entity_type = 'piece'
AND entity_id IN (SELECT id FROM pieces WHERE typepieceid = :id)",
['id' => self::MODEL_TYPE_ID],
);
$io->text(sprintf('✓ %d commentaire(s) mis à jour', $commentsUpdated));
// 2j. Supprimer les pièces originales
$deleted = $this->connection->executeStatement(
'DELETE FROM pieces WHERE typepieceid = :id',
['id' => self::MODEL_TYPE_ID],
);
$io->text(sprintf('✓ %d pièce(s) supprimée(s)', $deleted));
// 2k. Changer la catégorie du ModelType
$this->connection->executeStatement(
"UPDATE model_types SET category = 'COMPONENT', updatedat = :now WHERE id = :id",
['id' => self::MODEL_TYPE_ID, 'now' => $now],
);
$io->text('✓ ModelType "Moteur" passé en COMPONENT');
$this->connection->commit();
$io->success(sprintf('Conversion terminée : %d pièces → composants.', $converted));
return Command::SUCCESS;
} catch (Throwable $e) {
$this->connection->rollBack();
$io->error('Erreur — rollback effectué : '.$e->getMessage());
return Command::FAILURE;
}
}
/**
* @return array<string, int>
*/
private function countRelatedData(): array
{
$id = self::MODEL_TYPE_ID;
return [
'pieces' => (int) $this->connection->fetchOne(
'SELECT COUNT(*) FROM pieces WHERE typepieceid = :id',
['id' => $id],
),
'documents' => (int) $this->connection->fetchOne(
'SELECT COUNT(*) FROM documents WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)',
['id' => $id],
),
'custom_field_values' => (int) $this->connection->fetchOne(
'SELECT COUNT(*) FROM custom_field_values WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)',
['id' => $id],
),
'custom_fields' => (int) $this->connection->fetchOne(
'SELECT COUNT(*) FROM custom_fields WHERE typepieceid = :id',
['id' => $id],
),
'piece_constructeur_links' => (int) $this->connection->fetchOne(
'SELECT COUNT(*) FROM piece_constructeur_links WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)',
['id' => $id],
),
'composant_piece_slots (→ subcomponent_slots)' => (int) $this->connection->fetchOne(
'SELECT COUNT(*) FROM composant_piece_slots WHERE typepieceid = :id',
['id' => $id],
),
'skeleton_piece_requirements (→ subcomponent_reqs)' => (int) $this->connection->fetchOne(
'SELECT COUNT(*) FROM skeleton_piece_requirements WHERE typepieceid = :id',
['id' => $id],
),
];
}
}

View File

@@ -157,6 +157,18 @@
"symfony/mcp-bundle": {
"version": "v0.6.0"
},
"symfony/monolog-bundle": {
"version": "4.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.7",
"ref": "1b9efb10c54cb51c713a9391c9300ff8bceda459"
},
"files": [
"config/packages/monolog.yaml"
]
},
"symfony/property-info": {
"version": "8.0",
"recipe": {