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 >
*
2026-06-29 17:28:28 +00:00
* 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
2026-06-29 17:28:28 +00:00
* 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 .
2026-06-29 17:28:28 +00:00
*
* 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
}
2026-06-21 21:27:34 +00:00
// ── 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
}
}