dolibarr/test/phpunit/NamingContractTest.php

349 lines
14 KiB
PHP
Raw Permalink Normal View History

NEW : Replace MyObject MyModule occurences (#38370) * [COREMB] Centralize myobject/mymodule substitutions via NamingContract value object Introduce NamingContract (immutable value object) and NamingContractValidator to replace 6 scattered ad-hoc $arrayreplacement blocks across index.php and modulebuilder.lib.php. Fixes a critical bug where MYOBJECT token was commented out in initobject, leaving generated class files with unsubstituted placeholders. - NamingContract: canonical ordered substitution map (12 tokens with object, 7 module-only), applyTo() via str_replace (not make_substitutions, to avoid processing Dolibarr __(key)__ patterns in raw templates), applyToFilename() for lowercase-only filename substitution - StrictNamingContractValidator: post-generation scan for residual myobject/ mymodule tokens, skipping /* BEGIN/END MODULEBUILDER */ structural markers - modulebuilderValidateGeneratedFile() helper: non-blocking warnings with XSS-safe output via dol_escape_htmltag() - Collision guard: InvalidArgumentException if module === object name (case-insensitive), wrapped in try-catch at all instantiation sites - 24 PHPUnit tests, 52 assertions * fix(initobject): apply object substitution to all module PHP files After initmodule creates the module structure, files like index.php, lib/module.lib.php, admin/setup.php and class/api_module.class.php still contain myobject/mymodule placeholders. initobject now scans ALL PHP files in the module directory and applies the full NamingContract substitution, and deletes the orphan stats/myobject_index.php placeholder. * fix(initmodule): prevent array_merge from renumbering the '500000' key PHP casts numeric string keys like '500000' to int(500000) internally. array_merge() then renumbers integer keys starting from 0, turning '500000' into key 0. make_substitutions() then calls str_replace(0, ...) which casts 0 to the string '0', replacing EVERY zero digit in every generated file with the idmodule value. Fix: replace the bare '500000' key with '$this->numero = 500000' so that array_merge sees a non-numeric string key and preserves it as-is. * Fix initapi: apply header substitution after addObjectsToApiFile addObjectsToApiFile substitutes properties/constructor/includes via regex but leaves PHPDoc header and class declaration with mymodule/myobject tokens. Fix: apply $arrayreplacement (minus 'MYOBJECT' uppercase key) after addObjectsToApiFile to resolve the remaining residuals. 'MYOBJECT' is excluded to preserve the /* BEGIN MODULEBUILDER API MYOBJECT */ placeholder that addObjectsToApiFile relies on for future object additions. * Move NamingContractTest to core test directory NamingContract and NamingContractValidator are core modulebuilder classes; their tests belong alongside other Dolibarr core tests in test/phpunit/, not in htdocs/modulebuilder/test/phpunit/ (the custom-module pattern). Updated require_once paths accordingly. * fix useless file * fix(phpcs): add missing @param descriptions and function docblocks - NamingContract: merge adjacent string literals in exception message, add description to @param $content in applyTo() - NamingContractValidator: add descriptions to all bare @param tags in interface and StrictNamingContractValidator methods - NamingContractTest: add /** @return void */ docblocks to all test methods, merge unnecessary string concat on template variable * fix(phan/phpstan): remove readonly, add baselines for test file - NamingContract: replace readonly properties with plain public string (readonly requires PHP 8.1, phan minimum target is PHP 7.2) - dev/tools/phan/baseline.txt: add NamingContractTest.php suppression for PhanUndeclaredMethod (PHPUnit stubs not available in phan scope) - NamingContractTest.php: add @phpstan-ignore class.notFound on class declaration and method.notFound on each assertion call (PHPUnit not in phpstan bootstrap, consistent with CommonClassTest.class.php) * fix(phan/phpstan): suppress typed property warnings, NoopNew, class.notFound - baseline.txt: add PhanCompatibleTypedProperty for NamingContract.class.php (typed properties require PHP 7.4, phan min target is 7.2 — warning only but phan exits 1 on any issue including warnings) - baseline.txt: add PhanNoopNew for NamingContractTest.php (constructor called for exception side-effect in guard tests is intentional) - NamingContractTest.php: move @phpstan-ignore class.notFound to inline comment on class declaration (docblock placement not picked up by phpstan) * fix(phan/phpstan): add missing phan suppress on index.php l.574, fix phpstan class docblock - index.php l.574: add @phan-suppress-next-line PhanPluginSuspiciousParamPosition (same pattern as l.577 and l.587 for identical dolReplaceInFile calls where local variable name $destfile matches parameter #3 of the function) - NamingContractTest.php: use standalone /** @phpstan-ignore class.notFound */ docblock before class declaration (inline // comment not picked up by phpstan on class declarations; separate docblock matches CommonClassTest.class.php pattern) * Update baseline.txt * fix(phan): add phan-file-suppress on NamingContractTest for PHPUnit undeclared methods * fix(phan): suppress PhanNoopNew on expectException patterns in NamingContractTest --------- Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-05-25 12:52:38 +00:00
<?php
/* Copyright (C) 2026 ATM Consulting <support@atm-consulting.fr>
*
* This program is free software; you can redistribute it and/or modify
NEW : Replace MyObject MyModule occurences (#38370) * [COREMB] Centralize myobject/mymodule substitutions via NamingContract value object Introduce NamingContract (immutable value object) and NamingContractValidator to replace 6 scattered ad-hoc $arrayreplacement blocks across index.php and modulebuilder.lib.php. Fixes a critical bug where MYOBJECT token was commented out in initobject, leaving generated class files with unsubstituted placeholders. - NamingContract: canonical ordered substitution map (12 tokens with object, 7 module-only), applyTo() via str_replace (not make_substitutions, to avoid processing Dolibarr __(key)__ patterns in raw templates), applyToFilename() for lowercase-only filename substitution - StrictNamingContractValidator: post-generation scan for residual myobject/ mymodule tokens, skipping /* BEGIN/END MODULEBUILDER */ structural markers - modulebuilderValidateGeneratedFile() helper: non-blocking warnings with XSS-safe output via dol_escape_htmltag() - Collision guard: InvalidArgumentException if module === object name (case-insensitive), wrapped in try-catch at all instantiation sites - 24 PHPUnit tests, 52 assertions * fix(initobject): apply object substitution to all module PHP files After initmodule creates the module structure, files like index.php, lib/module.lib.php, admin/setup.php and class/api_module.class.php still contain myobject/mymodule placeholders. initobject now scans ALL PHP files in the module directory and applies the full NamingContract substitution, and deletes the orphan stats/myobject_index.php placeholder. * fix(initmodule): prevent array_merge from renumbering the '500000' key PHP casts numeric string keys like '500000' to int(500000) internally. array_merge() then renumbers integer keys starting from 0, turning '500000' into key 0. make_substitutions() then calls str_replace(0, ...) which casts 0 to the string '0', replacing EVERY zero digit in every generated file with the idmodule value. Fix: replace the bare '500000' key with '$this->numero = 500000' so that array_merge sees a non-numeric string key and preserves it as-is. * Fix initapi: apply header substitution after addObjectsToApiFile addObjectsToApiFile substitutes properties/constructor/includes via regex but leaves PHPDoc header and class declaration with mymodule/myobject tokens. Fix: apply $arrayreplacement (minus 'MYOBJECT' uppercase key) after addObjectsToApiFile to resolve the remaining residuals. 'MYOBJECT' is excluded to preserve the /* BEGIN MODULEBUILDER API MYOBJECT */ placeholder that addObjectsToApiFile relies on for future object additions. * Move NamingContractTest to core test directory NamingContract and NamingContractValidator are core modulebuilder classes; their tests belong alongside other Dolibarr core tests in test/phpunit/, not in htdocs/modulebuilder/test/phpunit/ (the custom-module pattern). Updated require_once paths accordingly. * fix useless file * fix(phpcs): add missing @param descriptions and function docblocks - NamingContract: merge adjacent string literals in exception message, add description to @param $content in applyTo() - NamingContractValidator: add descriptions to all bare @param tags in interface and StrictNamingContractValidator methods - NamingContractTest: add /** @return void */ docblocks to all test methods, merge unnecessary string concat on template variable * fix(phan/phpstan): remove readonly, add baselines for test file - NamingContract: replace readonly properties with plain public string (readonly requires PHP 8.1, phan minimum target is PHP 7.2) - dev/tools/phan/baseline.txt: add NamingContractTest.php suppression for PhanUndeclaredMethod (PHPUnit stubs not available in phan scope) - NamingContractTest.php: add @phpstan-ignore class.notFound on class declaration and method.notFound on each assertion call (PHPUnit not in phpstan bootstrap, consistent with CommonClassTest.class.php) * fix(phan/phpstan): suppress typed property warnings, NoopNew, class.notFound - baseline.txt: add PhanCompatibleTypedProperty for NamingContract.class.php (typed properties require PHP 7.4, phan min target is 7.2 — warning only but phan exits 1 on any issue including warnings) - baseline.txt: add PhanNoopNew for NamingContractTest.php (constructor called for exception side-effect in guard tests is intentional) - NamingContractTest.php: move @phpstan-ignore class.notFound to inline comment on class declaration (docblock placement not picked up by phpstan) * fix(phan/phpstan): add missing phan suppress on index.php l.574, fix phpstan class docblock - index.php l.574: add @phan-suppress-next-line PhanPluginSuspiciousParamPosition (same pattern as l.577 and l.587 for identical dolReplaceInFile calls where local variable name $destfile matches parameter #3 of the function) - NamingContractTest.php: use standalone /** @phpstan-ignore class.notFound */ docblock before class declaration (inline // comment not picked up by phpstan on class declarations; separate docblock matches CommonClassTest.class.php pattern) * Update baseline.txt * fix(phan): add phan-file-suppress on NamingContractTest for PHPUnit undeclared methods * fix(phan): suppress PhanNoopNew on expectException patterns in NamingContractTest --------- Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-05-25 12:52:38 +00:00
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
NEW : Replace MyObject MyModule occurences (#38370) * [COREMB] Centralize myobject/mymodule substitutions via NamingContract value object Introduce NamingContract (immutable value object) and NamingContractValidator to replace 6 scattered ad-hoc $arrayreplacement blocks across index.php and modulebuilder.lib.php. Fixes a critical bug where MYOBJECT token was commented out in initobject, leaving generated class files with unsubstituted placeholders. - NamingContract: canonical ordered substitution map (12 tokens with object, 7 module-only), applyTo() via str_replace (not make_substitutions, to avoid processing Dolibarr __(key)__ patterns in raw templates), applyToFilename() for lowercase-only filename substitution - StrictNamingContractValidator: post-generation scan for residual myobject/ mymodule tokens, skipping /* BEGIN/END MODULEBUILDER */ structural markers - modulebuilderValidateGeneratedFile() helper: non-blocking warnings with XSS-safe output via dol_escape_htmltag() - Collision guard: InvalidArgumentException if module === object name (case-insensitive), wrapped in try-catch at all instantiation sites - 24 PHPUnit tests, 52 assertions * fix(initobject): apply object substitution to all module PHP files After initmodule creates the module structure, files like index.php, lib/module.lib.php, admin/setup.php and class/api_module.class.php still contain myobject/mymodule placeholders. initobject now scans ALL PHP files in the module directory and applies the full NamingContract substitution, and deletes the orphan stats/myobject_index.php placeholder. * fix(initmodule): prevent array_merge from renumbering the '500000' key PHP casts numeric string keys like '500000' to int(500000) internally. array_merge() then renumbers integer keys starting from 0, turning '500000' into key 0. make_substitutions() then calls str_replace(0, ...) which casts 0 to the string '0', replacing EVERY zero digit in every generated file with the idmodule value. Fix: replace the bare '500000' key with '$this->numero = 500000' so that array_merge sees a non-numeric string key and preserves it as-is. * Fix initapi: apply header substitution after addObjectsToApiFile addObjectsToApiFile substitutes properties/constructor/includes via regex but leaves PHPDoc header and class declaration with mymodule/myobject tokens. Fix: apply $arrayreplacement (minus 'MYOBJECT' uppercase key) after addObjectsToApiFile to resolve the remaining residuals. 'MYOBJECT' is excluded to preserve the /* BEGIN MODULEBUILDER API MYOBJECT */ placeholder that addObjectsToApiFile relies on for future object additions. * Move NamingContractTest to core test directory NamingContract and NamingContractValidator are core modulebuilder classes; their tests belong alongside other Dolibarr core tests in test/phpunit/, not in htdocs/modulebuilder/test/phpunit/ (the custom-module pattern). Updated require_once paths accordingly. * fix useless file * fix(phpcs): add missing @param descriptions and function docblocks - NamingContract: merge adjacent string literals in exception message, add description to @param $content in applyTo() - NamingContractValidator: add descriptions to all bare @param tags in interface and StrictNamingContractValidator methods - NamingContractTest: add /** @return void */ docblocks to all test methods, merge unnecessary string concat on template variable * fix(phan/phpstan): remove readonly, add baselines for test file - NamingContract: replace readonly properties with plain public string (readonly requires PHP 8.1, phan minimum target is PHP 7.2) - dev/tools/phan/baseline.txt: add NamingContractTest.php suppression for PhanUndeclaredMethod (PHPUnit stubs not available in phan scope) - NamingContractTest.php: add @phpstan-ignore class.notFound on class declaration and method.notFound on each assertion call (PHPUnit not in phpstan bootstrap, consistent with CommonClassTest.class.php) * fix(phan/phpstan): suppress typed property warnings, NoopNew, class.notFound - baseline.txt: add PhanCompatibleTypedProperty for NamingContract.class.php (typed properties require PHP 7.4, phan min target is 7.2 — warning only but phan exits 1 on any issue including warnings) - baseline.txt: add PhanNoopNew for NamingContractTest.php (constructor called for exception side-effect in guard tests is intentional) - NamingContractTest.php: move @phpstan-ignore class.notFound to inline comment on class declaration (docblock placement not picked up by phpstan) * fix(phan/phpstan): add missing phan suppress on index.php l.574, fix phpstan class docblock - index.php l.574: add @phan-suppress-next-line PhanPluginSuspiciousParamPosition (same pattern as l.577 and l.587 for identical dolReplaceInFile calls where local variable name $destfile matches parameter #3 of the function) - NamingContractTest.php: use standalone /** @phpstan-ignore class.notFound */ docblock before class declaration (inline // comment not picked up by phpstan on class declarations; separate docblock matches CommonClassTest.class.php pattern) * Update baseline.txt * fix(phan): add phan-file-suppress on NamingContractTest for PHPUnit undeclared methods * fix(phan): suppress PhanNoopNew on expectException patterns in NamingContractTest --------- Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-05-25 12:52:38 +00:00
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
NEW : Replace MyObject MyModule occurences (#38370) * [COREMB] Centralize myobject/mymodule substitutions via NamingContract value object Introduce NamingContract (immutable value object) and NamingContractValidator to replace 6 scattered ad-hoc $arrayreplacement blocks across index.php and modulebuilder.lib.php. Fixes a critical bug where MYOBJECT token was commented out in initobject, leaving generated class files with unsubstituted placeholders. - NamingContract: canonical ordered substitution map (12 tokens with object, 7 module-only), applyTo() via str_replace (not make_substitutions, to avoid processing Dolibarr __(key)__ patterns in raw templates), applyToFilename() for lowercase-only filename substitution - StrictNamingContractValidator: post-generation scan for residual myobject/ mymodule tokens, skipping /* BEGIN/END MODULEBUILDER */ structural markers - modulebuilderValidateGeneratedFile() helper: non-blocking warnings with XSS-safe output via dol_escape_htmltag() - Collision guard: InvalidArgumentException if module === object name (case-insensitive), wrapped in try-catch at all instantiation sites - 24 PHPUnit tests, 52 assertions * fix(initobject): apply object substitution to all module PHP files After initmodule creates the module structure, files like index.php, lib/module.lib.php, admin/setup.php and class/api_module.class.php still contain myobject/mymodule placeholders. initobject now scans ALL PHP files in the module directory and applies the full NamingContract substitution, and deletes the orphan stats/myobject_index.php placeholder. * fix(initmodule): prevent array_merge from renumbering the '500000' key PHP casts numeric string keys like '500000' to int(500000) internally. array_merge() then renumbers integer keys starting from 0, turning '500000' into key 0. make_substitutions() then calls str_replace(0, ...) which casts 0 to the string '0', replacing EVERY zero digit in every generated file with the idmodule value. Fix: replace the bare '500000' key with '$this->numero = 500000' so that array_merge sees a non-numeric string key and preserves it as-is. * Fix initapi: apply header substitution after addObjectsToApiFile addObjectsToApiFile substitutes properties/constructor/includes via regex but leaves PHPDoc header and class declaration with mymodule/myobject tokens. Fix: apply $arrayreplacement (minus 'MYOBJECT' uppercase key) after addObjectsToApiFile to resolve the remaining residuals. 'MYOBJECT' is excluded to preserve the /* BEGIN MODULEBUILDER API MYOBJECT */ placeholder that addObjectsToApiFile relies on for future object additions. * Move NamingContractTest to core test directory NamingContract and NamingContractValidator are core modulebuilder classes; their tests belong alongside other Dolibarr core tests in test/phpunit/, not in htdocs/modulebuilder/test/phpunit/ (the custom-module pattern). Updated require_once paths accordingly. * fix useless file * fix(phpcs): add missing @param descriptions and function docblocks - NamingContract: merge adjacent string literals in exception message, add description to @param $content in applyTo() - NamingContractValidator: add descriptions to all bare @param tags in interface and StrictNamingContractValidator methods - NamingContractTest: add /** @return void */ docblocks to all test methods, merge unnecessary string concat on template variable * fix(phan/phpstan): remove readonly, add baselines for test file - NamingContract: replace readonly properties with plain public string (readonly requires PHP 8.1, phan minimum target is PHP 7.2) - dev/tools/phan/baseline.txt: add NamingContractTest.php suppression for PhanUndeclaredMethod (PHPUnit stubs not available in phan scope) - NamingContractTest.php: add @phpstan-ignore class.notFound on class declaration and method.notFound on each assertion call (PHPUnit not in phpstan bootstrap, consistent with CommonClassTest.class.php) * fix(phan/phpstan): suppress typed property warnings, NoopNew, class.notFound - baseline.txt: add PhanCompatibleTypedProperty for NamingContract.class.php (typed properties require PHP 7.4, phan min target is 7.2 — warning only but phan exits 1 on any issue including warnings) - baseline.txt: add PhanNoopNew for NamingContractTest.php (constructor called for exception side-effect in guard tests is intentional) - NamingContractTest.php: move @phpstan-ignore class.notFound to inline comment on class declaration (docblock placement not picked up by phpstan) * fix(phan/phpstan): add missing phan suppress on index.php l.574, fix phpstan class docblock - index.php l.574: add @phan-suppress-next-line PhanPluginSuspiciousParamPosition (same pattern as l.577 and l.587 for identical dolReplaceInFile calls where local variable name $destfile matches parameter #3 of the function) - NamingContractTest.php: use standalone /** @phpstan-ignore class.notFound */ docblock before class declaration (inline // comment not picked up by phpstan on class declarations; separate docblock matches CommonClassTest.class.php pattern) * Update baseline.txt * fix(phan): add phan-file-suppress on NamingContractTest for PHPUnit undeclared methods * fix(phan): suppress PhanNoopNew on expectException patterns in NamingContractTest --------- Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-05-25 12:52:38 +00:00
*/
/**
* \file test/phpunit/NamingContractTest.php
* \ingroup modulebuilder
* \brief Unit tests for NamingContract and StrictNamingContractValidator.
*/
require_once dirname(__FILE__) . '/../../htdocs/modulebuilder/class/NamingContract.class.php';
require_once dirname(__FILE__) . '/../../htdocs/modulebuilder/class/NamingContractValidator.class.php';
/**
* @backupGlobals disabled
* @phan-file-suppress PhanUndeclaredClass
* @phan-file-suppress PhanUndeclaredExtendedClass
* @phan-file-suppress PhanUndeclaredMethod
* @phan-file-suppress PhanNoopNew
*/
/** @phpstan-ignore class.notFound */
class NamingContractTest extends \PHPUnit\Framework\TestCase
{
// ── NamingContract — properties ───────────────────────────────────────
/**
* @return void
*/
public function testNormalizesModuleFromLowercase(): void
{
$nc = new NamingContract('invoice');
$this->assertSame('Invoice', $nc->moduleNameCase); // @phpstan-ignore method.notFound
$this->assertSame('invoice', $nc->moduleNameLower); // @phpstan-ignore method.notFound
$this->assertSame('INVOICE', $nc->moduleNameUpper); // @phpstan-ignore method.notFound
}
/**
* @return void
*/
public function testNormalizesModuleFromPascalCase(): void
{
$nc = new NamingContract('Invoice');
$this->assertSame('Invoice', $nc->moduleNameCase); // @phpstan-ignore method.notFound
$this->assertSame('invoice', $nc->moduleNameLower); // @phpstan-ignore method.notFound
$this->assertSame('INVOICE', $nc->moduleNameUpper); // @phpstan-ignore method.notFound
}
/**
* @return void
*/
public function testNormalizesObjectFromLowercase(): void
{
$nc = new NamingContract('invoice', 'request');
$this->assertSame('Request', $nc->objectNameCase); // @phpstan-ignore method.notFound
$this->assertSame('request', $nc->objectNameLower); // @phpstan-ignore method.notFound
$this->assertSame('REQUEST', $nc->objectNameUpper); // @phpstan-ignore method.notFound
}
/**
* @return void
*/
public function testPreservesCompoundPascalCaseObject(): void
{
$nc = new NamingContract('Invoice', 'InvoiceRequest');
$this->assertSame('InvoiceRequest', $nc->objectNameCase); // @phpstan-ignore method.notFound
$this->assertSame('invoicerequest', $nc->objectNameLower); // @phpstan-ignore method.notFound
$this->assertSame('INVOICEREQUEST', $nc->objectNameUpper); // @phpstan-ignore method.notFound
}
// ── NamingContract — map ──────────────────────────────────────────────
/**
* @return void
*/
public function testSubstitutionMapHasTwelveKeysWithObject(): void
{
$nc = new NamingContract('mymodule', 'myobject');
$this->assertCount(12, $nc->getSubstitutionMap()); // @phpstan-ignore method.notFound
}
/**
* @return void
*/
public function testSubstitutionMapHasSevenKeysModuleOnly(): void
{
$nc = new NamingContract('mymodule');
$this->assertCount(7, $nc->getSubstitutionMap()); // @phpstan-ignore method.notFound
}
/**
* @return void
*/
public function testSubstitutionMapOrderUpperBeforeLower(): void
{
$nc = new NamingContract('mymodule', 'myobject');
$keys = array_keys($nc->getSubstitutionMap());
$posUpper = array_search('MYOBJECT', $keys, true);
$posCase = array_search('MyObject', $keys, true);
$posLower = array_search('myobject', $keys, true);
$this->assertIsInt($posUpper); // @phpstan-ignore method.notFound
$this->assertIsInt($posCase); // @phpstan-ignore method.notFound
$this->assertIsInt($posLower); // @phpstan-ignore method.notFound
$this->assertLessThan($posCase, $posUpper, 'MYOBJECT must precede MyObject'); // @phpstan-ignore method.notFound
$this->assertLessThan($posLower, $posCase, 'MyObject must precede myobject'); // @phpstan-ignore method.notFound
}
// ── NamingContract — applyTo ──────────────────────────────────────────
/**
* @return void
*/
public function testApplyToReplacesAllTwelveVariants(): void
{
$nc = new NamingContract('invoice', 'request');
$template = 'MYMODULE MyModule My module my module Mon module mon module mymodule MYOBJECT MyObject My Object my object myobject';
$result = $nc->applyTo($template);
$this->assertStringNotContainsStringIgnoringCase('mymodule', $result); // @phpstan-ignore method.notFound
$this->assertStringNotContainsStringIgnoringCase('myobject', $result); // @phpstan-ignore method.notFound
$this->assertStringContainsString('INVOICE', $result); // @phpstan-ignore method.notFound
$this->assertStringContainsString('Invoice', $result); // @phpstan-ignore method.notFound
$this->assertStringContainsString('invoice', $result); // @phpstan-ignore method.notFound
$this->assertStringContainsString('REQUEST', $result); // @phpstan-ignore method.notFound
$this->assertStringContainsString('Request', $result); // @phpstan-ignore method.notFound
$this->assertStringContainsString('request', $result); // @phpstan-ignore method.notFound
}
/**
* @return void
*/
public function testApplyToPreservesModulebuilderPermissionsMarker(): void
{
$nc = new NamingContract('invoice', 'request');
$content = '/* BEGIN MODULEBUILDER PERMISSIONS */ code /* END MODULEBUILDER PERMISSIONS */';
$result = $nc->applyTo($content);
$this->assertStringContainsString('/* BEGIN MODULEBUILDER PERMISSIONS */', $result); // @phpstan-ignore method.notFound
$this->assertStringContainsString('/* END MODULEBUILDER PERMISSIONS */', $result); // @phpstan-ignore method.notFound
}
/**
* @return void
*/
public function testApplyToDoesNotAlterUntokenizedContent(): void
{
$nc = new NamingContract('invoice', 'request');
$content = 'class CommonObject extends DolibarrObject {}';
$this->assertSame($content, $nc->applyTo($content)); // @phpstan-ignore method.notFound
}
// ── Generated list template — trackid placeholder (#32460 item 14) ────
/**
* The generated list page must derive its e-mail trackid from the object name,
* exactly like the card page (myobject_card.php) and core list pages
* (comm/propal/list.php => 'pro', societe/list.php => 'thi'). The legacy literal
* 'xxxx' placeholder is not a substitution token, so it used to leak verbatim into
* every generated module and collide across modules in the mass e-mailing trackid.
* Regression guard for #32460 (item 14, e-mail/trackid handling).
*
* @return void
*/
public function testListTemplateTrackidIsSubstituted(): void
{
$template = file_get_contents(dirname(__FILE__) . '/../../htdocs/modulebuilder/template/myobject_list.php');
$this->assertIsString($template); // @phpstan-ignore method.notFound
// The non-substitutable 'xxxx' placeholder must no longer carry the trackid.
$this->assertStringNotContainsString("\$trackid = 'xxxx'", $template, "The list template must not carry the 'xxxx' trackid placeholder"); // @phpstan-ignore method.notFound
// After generation the trackid derives from the object name, like the card page and core.
$nc = new NamingContract('invoice', 'request');
$result = $nc->applyTo($template);
$this->assertStringContainsString("\$trackid = 'request'.\$object->id;", $result, 'Generated trackid must derive from the object name'); // @phpstan-ignore method.notFound
}
NEW : Replace MyObject MyModule occurences (#38370) * [COREMB] Centralize myobject/mymodule substitutions via NamingContract value object Introduce NamingContract (immutable value object) and NamingContractValidator to replace 6 scattered ad-hoc $arrayreplacement blocks across index.php and modulebuilder.lib.php. Fixes a critical bug where MYOBJECT token was commented out in initobject, leaving generated class files with unsubstituted placeholders. - NamingContract: canonical ordered substitution map (12 tokens with object, 7 module-only), applyTo() via str_replace (not make_substitutions, to avoid processing Dolibarr __(key)__ patterns in raw templates), applyToFilename() for lowercase-only filename substitution - StrictNamingContractValidator: post-generation scan for residual myobject/ mymodule tokens, skipping /* BEGIN/END MODULEBUILDER */ structural markers - modulebuilderValidateGeneratedFile() helper: non-blocking warnings with XSS-safe output via dol_escape_htmltag() - Collision guard: InvalidArgumentException if module === object name (case-insensitive), wrapped in try-catch at all instantiation sites - 24 PHPUnit tests, 52 assertions * fix(initobject): apply object substitution to all module PHP files After initmodule creates the module structure, files like index.php, lib/module.lib.php, admin/setup.php and class/api_module.class.php still contain myobject/mymodule placeholders. initobject now scans ALL PHP files in the module directory and applies the full NamingContract substitution, and deletes the orphan stats/myobject_index.php placeholder. * fix(initmodule): prevent array_merge from renumbering the '500000' key PHP casts numeric string keys like '500000' to int(500000) internally. array_merge() then renumbers integer keys starting from 0, turning '500000' into key 0. make_substitutions() then calls str_replace(0, ...) which casts 0 to the string '0', replacing EVERY zero digit in every generated file with the idmodule value. Fix: replace the bare '500000' key with '$this->numero = 500000' so that array_merge sees a non-numeric string key and preserves it as-is. * Fix initapi: apply header substitution after addObjectsToApiFile addObjectsToApiFile substitutes properties/constructor/includes via regex but leaves PHPDoc header and class declaration with mymodule/myobject tokens. Fix: apply $arrayreplacement (minus 'MYOBJECT' uppercase key) after addObjectsToApiFile to resolve the remaining residuals. 'MYOBJECT' is excluded to preserve the /* BEGIN MODULEBUILDER API MYOBJECT */ placeholder that addObjectsToApiFile relies on for future object additions. * Move NamingContractTest to core test directory NamingContract and NamingContractValidator are core modulebuilder classes; their tests belong alongside other Dolibarr core tests in test/phpunit/, not in htdocs/modulebuilder/test/phpunit/ (the custom-module pattern). Updated require_once paths accordingly. * fix useless file * fix(phpcs): add missing @param descriptions and function docblocks - NamingContract: merge adjacent string literals in exception message, add description to @param $content in applyTo() - NamingContractValidator: add descriptions to all bare @param tags in interface and StrictNamingContractValidator methods - NamingContractTest: add /** @return void */ docblocks to all test methods, merge unnecessary string concat on template variable * fix(phan/phpstan): remove readonly, add baselines for test file - NamingContract: replace readonly properties with plain public string (readonly requires PHP 8.1, phan minimum target is PHP 7.2) - dev/tools/phan/baseline.txt: add NamingContractTest.php suppression for PhanUndeclaredMethod (PHPUnit stubs not available in phan scope) - NamingContractTest.php: add @phpstan-ignore class.notFound on class declaration and method.notFound on each assertion call (PHPUnit not in phpstan bootstrap, consistent with CommonClassTest.class.php) * fix(phan/phpstan): suppress typed property warnings, NoopNew, class.notFound - baseline.txt: add PhanCompatibleTypedProperty for NamingContract.class.php (typed properties require PHP 7.4, phan min target is 7.2 — warning only but phan exits 1 on any issue including warnings) - baseline.txt: add PhanNoopNew for NamingContractTest.php (constructor called for exception side-effect in guard tests is intentional) - NamingContractTest.php: move @phpstan-ignore class.notFound to inline comment on class declaration (docblock placement not picked up by phpstan) * fix(phan/phpstan): add missing phan suppress on index.php l.574, fix phpstan class docblock - index.php l.574: add @phan-suppress-next-line PhanPluginSuspiciousParamPosition (same pattern as l.577 and l.587 for identical dolReplaceInFile calls where local variable name $destfile matches parameter #3 of the function) - NamingContractTest.php: use standalone /** @phpstan-ignore class.notFound */ docblock before class declaration (inline // comment not picked up by phpstan on class declarations; separate docblock matches CommonClassTest.class.php pattern) * Update baseline.txt * fix(phan): add phan-file-suppress on NamingContractTest for PHPUnit undeclared methods * fix(phan): suppress PhanNoopNew on expectException patterns in NamingContractTest --------- Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-05-25 12:52:38 +00:00
// ── NamingContract — applyToFilename ─────────────────────────────────
/**
* @return void
*/
public function testApplyToFilenameReplacesLowercaseTokens(): void
{
$nc = new NamingContract('invoice', 'request');
$this->assertSame('invoice_request.lib.php', $nc->applyToFilename('mymodule_myobject.lib.php')); // @phpstan-ignore method.notFound
$this->assertSame('llx_invoice_request.sql', $nc->applyToFilename('llx_mymodule_myobject.sql')); // @phpstan-ignore method.notFound
$this->assertSame('admin/request_extrafields.php', $nc->applyToFilename('admin/myobject_extrafields.php')); // @phpstan-ignore method.notFound
}
/**
* @return void
*/
public function testApplyToFilenameModuleOnlyIgnoresObjectToken(): void
{
$nc = new NamingContract('invoice');
$this->assertSame('invoice_myobject.lib.php', $nc->applyToFilename('mymodule_myobject.lib.php')); // @phpstan-ignore method.notFound
}
// ── NamingContract — module-only contract ────────────────────────────
/**
* @return void
*/
public function testModuleOnlyContractHasEmptyObjectProps(): void
{
$nc = new NamingContract('invoice');
$this->assertSame('', $nc->objectNameCase); // @phpstan-ignore method.notFound
$this->assertSame('', $nc->objectNameLower); // @phpstan-ignore method.notFound
$this->assertSame('', $nc->objectNameUpper); // @phpstan-ignore method.notFound
}
// ── NamingContract — guard ────────────────────────────────────────────
/**
* @return void
*/
public function testCollisionGuardThrowsOnIdenticalNames(): void
{
$this->expectException(\InvalidArgumentException::class); // @phpstan-ignore method.notFound
new NamingContract('invoice', 'invoice');
}
/**
* @return void
*/
public function testCollisionGuardIsCaseInsensitive(): void
{
$this->expectException(\InvalidArgumentException::class); // @phpstan-ignore method.notFound
new NamingContract('Invoice', 'INVOICE');
}
// ── StrictNamingContractValidator — validateContent ──────────────────
/**
* @return void
*/
public function testValidatorCatchesResidualMyobjectLowercase(): void
{
$validator = new StrictNamingContractValidator();
$errors = $validator->validateContent('$obj = new myobject($db);', 'test.php');
$this->assertNotEmpty($errors); // @phpstan-ignore method.notFound
$this->assertStringContainsString('myobject', $errors[0]); // @phpstan-ignore method.notFound
}
/**
* @return void
*/
public function testValidatorCatchesResidualMyobjectMixedCase(): void
{
$validator = new StrictNamingContractValidator();
$errors = $validator->validateContent('class MyObject extends CommonObject', 'test.php');
$this->assertNotEmpty($errors); // @phpstan-ignore method.notFound
}
/**
* @return void
*/
public function testValidatorCatchesResidualMymodule(): void
{
$validator = new StrictNamingContractValidator();
$errors = $validator->validateContent("isModEnabled('mymodule')", 'test.php');
$this->assertNotEmpty($errors); // @phpstan-ignore method.notFound
$this->assertStringContainsString('mymodule', $errors[0]); // @phpstan-ignore method.notFound
}
/**
* @return void
*/
public function testValidatorAcceptsNonRenamableBeginMarker(): void
{
$validator = new StrictNamingContractValidator();
$content = "/* BEGIN MODULEBUILDER API MYOBJECT */\n\t/* END MODULEBUILDER API MYOBJECT */";
$errors = $validator->validateContent($content, 'test.php');
$this->assertEmpty($errors); // @phpstan-ignore method.notFound
}
/**
* @return void
*/
public function testValidatorAcceptsCleanContent(): void
{
$validator = new StrictNamingContractValidator();
$errors = $validator->validateContent('$obj = new Invoice($db);', 'test.php');
$this->assertEmpty($errors); // @phpstan-ignore method.notFound
}
// ── StrictNamingContractValidator — other methods ────────────────────
/**
* @return void
*/
public function testValidatorValidatesMatchingTriggerFilename(): void
{
$nc = new NamingContract('Invoice');
$validator = new StrictNamingContractValidator();
$this->assertTrue( // @phpstan-ignore method.notFound
$validator->validateTriggerFilename('interface_99_modInvoice_InvoiceTriggers.class.php', $nc)
);
}
/**
* @return void
*/
public function testValidatorRejectsResidualTriggerFilename(): void
{
$nc = new NamingContract('Invoice');
$validator = new StrictNamingContractValidator();
$this->assertFalse( // @phpstan-ignore method.notFound
$validator->validateTriggerFilename('interface_99_modMyModule_MyModuleTriggers.class.php', $nc)
);
}
/**
* @return void
*/
public function testValidatorValidatesMatchingClassName(): void
{
$nc = new NamingContract('Invoice', 'Request');
$validator = new StrictNamingContractValidator();
$this->assertTrue($validator->validateClassName('Request', $nc)); // @phpstan-ignore method.notFound
$this->assertFalse($validator->validateClassName('MyObject', $nc)); // @phpstan-ignore method.notFound
}
/**
* @return void
*/
public function testValidatorValidatesRightsKey(): void
{
$nc = new NamingContract('invoice', 'request');
$validator = new StrictNamingContractValidator();
$this->assertTrue($validator->validateRightsKey('invoice.request.read', $nc)); // @phpstan-ignore method.notFound
$this->assertFalse($validator->validateRightsKey('mymodule.myobject.read', $nc)); // @phpstan-ignore method.notFound
}
}