NEW: ModuleBuilder - selectable object tabs (+ fixes for multi-object generation) (#38570)

* NEW : add ModuleBuilder object-tabs map and filter helpers

Add getModuleBuilderObjectTabs() (source-of-truth map of optional object
tabs) and filterEnabledTabs() (sanitizes user-requested tab keys against the
map). Covered by ModuleBuilderLibTest.

Next: add BEGIN/END markers around tab flags and blocks in template lib (Task 2)

* NEW : wrap object tab flags and blocks with MODULEBUILDER markers

Surround each optional tab flag declaration (TABFLAG) and each prepareHead tab
block (TAB) with BEGIN/END MODULEBUILDER comment markers, so the generator can
purge a whole tab atomically while keeping $h and $head intact. Card tab is
not wrapped (always generated).

Next: add tab selection checkboxes to the newobject form in modulebuilder/index.php (Task 3)

* NEW : add object tab selection checkboxes to newobject form

Render one checkbox per optional tab (contact, note, document, agenda) in the
ModuleBuilder newobject form, all checked by default and reflecting posted
state on redisplay.

Next: handle enabledtab in initobject — exclude files, purge blocks, hardcode flags (Task 4)

* NEW : apply object tab selection during object generation

Excludes page files of unselected tabs, purges their prepareHead flag and
block via MODULEBUILDER markers, and hardcodes the show flag to 1 for selected
tabs. Emits a best-effort warning when regenerating an existing object.

Next: add language keys EnabledTabsForObject(Help) + WarningTabSelectionOnRegeneration (Task 5)

* NEW : add language keys for object tab selection

Next: add ChangeLog entry (Task 6)

* DOC : changelog entry for ModuleBuilder selectable object tabs

* FIX : purge agenda card widget when agenda tab is excluded

Audit finding: myobject_card.php hardcodes a 'SeeAll' link to myobject_agenda.php
in an event widget. When the agenda tab is excluded the page is not generated,
leaving a latent dead link if the widget is enabled. Wrap the widget with
MODULEBUILDER TAB AGENDA markers and purge it from the generated card when the
agenda tab is not selected.

Next: functional verification (step 5 of dev workflow)

* FIX : check file operation results when applying object tab selection

PR review: align with the nogeneratelines pattern by checking the return of
removePatternFromFile/dolReplaceInFile and raising $error + dol_syslog with the
failing tab/file context, instead of ignoring silent write failures during
object generation. Add PR reference to the changelog entry.

* FIX : do not abort object generation when nogeneratelines targets a missing file

The nogeneratelines handler purges the MODULEBUILDER LINES block from class,
API and card files. When API generation is disabled the api_<module>.class.php
file does not exist, so removePatternFromFile returns false and $error was
incremented, which skipped the whole success path (name substitution,
rebuildObjectClass, $tabobj assignment). Generated classes kept the literal
'class MyObject', so dolGetListOfObjectClasses listed every object as MyObject.
Guard each purge with file_exists so a missing optional file is skipped.

* FIX : keep module descriptor out of the blanket name substitution

The 'substitute all module php files' pass applied the object/module name map
to every .php file including the module descriptor. That rewrote the persistent
/* ... MODULEBUILDER TOPMENU/LEFTMENU MYOBJECT */ marker placeholders to the
first object name, so generating a second object could no longer find them
(checkExistComment returned -1, 'comments not found for section Menus' warning)
and its menu entries were not inserted. Skip the descriptor in that loop: its
module name is already resolved by initmodule and its object entries are added
by the dedicated menu/permission blocks.

* FIX : preserve MODULEBUILDER markers when substituting the module descriptor

The previous commit only skipped the descriptor in the secondary substitution
pass, but the primary pass (over $filetogenerate, which includes the descriptor)
still rewrote its persistent /* ... MODULEBUILDER TOPMENU/LEFTMENU MYOBJECT */
markers to the first object name. Those markers cannot simply be excluded from
substitution because the descriptor also has functional MYOBJECT/MYMODULE tokens
(e.g. the MYMODULE_MYOBJECT_ADDON numbering constant) that must be resolved.

Add dolReplaceInFilePreservingModuleBuilderMarkers(): it hides every MODULEBUILDER
marker behind a sentinel, applies the substitution, then restores the markers
verbatim. Use it for the descriptor in the primary pass; the descriptor stays
excluded from the secondary pass. Generating a second object now finds the
markers (checkExistComment) and inserts its menu entries without warning.

Verified by CLI: markers preserved across two successive objects, while content
(rights, exports, addon constant) is correctly substituted.

* FIX : silence phan on the new ModuleBuilder test and typed closure

CI phan (diff-only) flagged the freshly added test file: PhanUndeclaredExtendedClass
(\CommonClassTest) and PhanUndeclaredMethod (assertSame), like the sibling
NamingContractTest. Add the same @phan-file-suppress block (plus
PhanTypeMismatchArgumentProbablyReal for the non-array guard test case). Also
type the preg_replace_callback parameter (array $matches) to clear
PhanPluginUnknownClosureParamType. phpstan is already green; no phpstan
annotations added.

* FIX : drop closure in marker-preserving substitution to satisfy phan

phan's PhanPluginUnknownArrayClosureParamType wanted key/value types on the
preg_replace_callback closure parameter. Replace the callback with preg_match_all
plus a str_replace loop over the unique markers: same behaviour, no closure,
no plugin warning.

---------

Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
This commit is contained in:
VIAL-GOUTEYRON Quentin 2026-06-09 19:04:00 +02:00 committed by GitHub
parent bae2ca251c
commit 3216098ab5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 258 additions and 2 deletions

View file

@ -176,6 +176,7 @@ For developers:
---------------
NEW: Use another hash algorithm v2 based on sha256/hmac for immutable logs (#37725)
NEW: Replace MyObject MyModule occurrences (#38370)
NEW: ModuleBuilder - Allow selecting which optional tabs (contact, note, document, agenda) are generated for an object (#38570)
NEW: add hooks in reception card (#37214)
NEW: add new hook in BonPrelevement::EnregDestinataireSEPA() function (#37419)
NEW: Add hook selectForFormsListUrl in Form::selectForForms (#37447)

View file

@ -1465,3 +1465,85 @@ function countItemsInDirectory($path, $type = 1)
}
return $count;
}
/**
* Return the map of optional tabs that can be generated for a ModuleBuilder object.
* The CARD tab is always generated and is therefore not listed here.
* HISTORY is an alias of AGENDA (object event history is the agenda tab in Dolibarr).
*
* @return array<string,array{file:string,var:string,marker:string,label:string}> Map: tab key => metadata
*/
function getModuleBuilderObjectTabs()
{
return array(
'contact' => array('file' => 'myobject_contact.php', 'var' => 'showtabofpagecontact', 'marker' => 'CONTACT', 'label' => 'Contacts'),
'note' => array('file' => 'myobject_note.php', 'var' => 'showtabofpagenote', 'marker' => 'NOTE', 'label' => 'Notes'),
'document' => array('file' => 'myobject_document.php', 'var' => 'showtabofpagedocument', 'marker' => 'DOCUMENT', 'label' => 'Documents'),
'agenda' => array('file' => 'myobject_agenda.php', 'var' => 'showtabofpageagenda', 'marker' => 'AGENDA', 'label' => 'Events'),
);
}
/**
* Filter a list of requested tab keys against the known optional tabs map.
* Protects against injection of unknown keys, removes duplicates, normalizes order.
*
* @param string[] $requested Raw tab keys requested by the user (e.g. from GETPOST array)
* @param array<string,array{file:string,var:string,marker:string,label:string}> $map Map from getModuleBuilderObjectTabs()
* @return string[] Sanitized list of valid tab keys, in map order
*/
function filterEnabledTabs($requested, $map)
{
$valid = array();
if (!is_array($requested) || empty($requested)) {
return $valid;
}
foreach (array_keys($map) as $tabkey) {
if (in_array($tabkey, $requested, true)) {
$valid[] = $tabkey;
}
}
return $valid;
}
/**
* Apply substitutions to a module descriptor file while preserving the MODULEBUILDER comment markers.
* Markers such as "BEGIN MODULEBUILDER LEFTMENU MYOBJECT" must keep their MYOBJECT/MYMODULE placeholder
* so that generating subsequent objects can still locate them (see checkExistComment()). A blanket
* substitution would rewrite them to the first object name and break the generation of further objects.
*
* @param string $file Path to the module descriptor file
* @param array<string,string> $arrayreplacement Substitution map (search => replace), applied as literal strings
* @return int 1 on success, -1 on read/write error
*/
function dolReplaceInFilePreservingModuleBuilderMarkers($file, $arrayreplacement)
{
if (!file_exists($file)) {
return -1;
}
$content = file_get_contents($file);
if ($content === false) {
return -1;
}
// Hide every "/* BEGIN|END MODULEBUILDER ... */" marker behind a sentinel before substituting
$foundmarkers = array();
preg_match_all('/\/\*\s*(?:BEGIN|END) MODULEBUILDER [^*]*\*\//', $content, $foundmarkers);
$sentinels = array();
foreach (array_values(array_unique($foundmarkers[0])) as $index => $marker) {
$key = "\0MODULEBUILDERMARKER".$index."\0";
$sentinels[$key] = $marker;
$content = str_replace($marker, $key, $content);
}
$content = str_replace(array_keys($arrayreplacement), array_values($arrayreplacement), $content);
// Restore the protected markers untouched
if (!empty($sentinels)) {
$content = strtr($content, $sentinels);
}
if (file_put_contents($file, $content) === false) {
return -1;
}
return 1;
}

View file

@ -172,6 +172,9 @@ GeneratePermissions=I want to manage permissions on this object
GeneratePermissionsHelp=If you check this, some code will be added to manage permissions to read, write and delete record of the objects
NoGenerateLines=I don't want to manage lines on this object
NoGenerateLinesHelp=If you check this, some code will be removed to manage lines of the objects
EnabledTabsForObject=Tabs to generate for this object
EnabledTabsForObjectHelp=Only the selected tabs will generate their page file and tab link. The card tab is always generated. History is the agenda tab.
WarningTabSelectionOnRegeneration=This object already exists. Tab selection changes are applied on a best-effort basis on regeneration: already generated files are not overwritten or deleted, and a previously removed tab block cannot be re-injected automatically.
PermissionDeletedSuccesfuly=Permission has been successfully removed
PermissionUpdatedSuccesfuly=Permission has been successfully updated
PermissionAddedSuccesfuly=Permission has been successfully added

View file

@ -170,6 +170,9 @@ DefinePropertiesFromExistingTableDesc=Si une table dans la base de données (pou
DefinePropertiesFromExistingTableDesc2=Laisser vide si la table n'existe pas encore. Le générateur de code utilisera différents types de champs pour créer un exemple de table que vous pourrez modifier ultérieurement.
GeneratePermissions=Je souhaite gérer les permissions sur cet objet
GeneratePermissionsHelp=Si vous cochez ceci, du code sera ajouté pour gérer les permissions de lecture, d'écriture et de suppression des enregistrements des objets.
EnabledTabsForObject=Onglets à générer pour cet objet
EnabledTabsForObjectHelp=Seuls les onglets sélectionnés généreront leur fichier de page et leur lien d'onglet. L'onglet fiche est toujours généré. L'historique correspond à l'onglet agenda.
WarningTabSelectionOnRegeneration=Cet objet existe déjà. Les changements de sélection d'onglets sont appliqués au mieux lors d'une régénération : les fichiers déjà générés ne sont ni écrasés ni supprimés, et un bloc d'onglet précédemment retiré ne peut pas être réinjecté automatiquement.
PermissionDeletedSuccesfuly=Les permissions ont été retirées avec succès
PermissionUpdatedSuccesfuly=Les permissions ont été mises à jour avec succès
PermissionAddedSuccesfuly=Les permissions ont été ajoutées avec succès

View file

@ -1151,6 +1151,10 @@ if ($dirins && $action == 'initobject' && $module && $objectname) { // Test on
$srcdir = DOL_DOCUMENT_ROOT.'/modulebuilder/template';
$destdir = $dirins.'/'.strtolower($module);
// Optional tabs selected by user, and detection of an already generated object (for idempotence warning)
$enabledtabs = filterEnabledTabs(GETPOST('enabledtab', 'array'), getModuleBuilderObjectTabs());
$objectalreadyexists = dol_is_file($destdir.'/class/'.strtolower($objectname).'.class.php');
// The dir was not created by init
dol_mkdir($destdir.'/class');
dol_mkdir($destdir.'/img');
@ -1469,6 +1473,13 @@ if ($dirins && $action == 'initobject' && $module && $objectname) { // Test on
$filetogenerate[$templateFile] = $ncObj->applyToFilename($templateFile);
}
// Exclude tab page files for tabs not selected by user
foreach (getModuleBuilderObjectTabs() as $tabkey => $tabinfo) {
if (!in_array($tabkey, $enabledtabs, true)) {
unset($filetogenerate[$tabinfo['file']]);
}
}
if (GETPOST('includerefgeneration', 'aZ09')) {
dol_mkdir($destdir.'/core/modules/'.strtolower($module));
@ -1727,13 +1738,50 @@ if ($dirins && $action == 'initobject' && $module && $objectname) { // Test on
// Pattern to remove everything between the tags
$pattern = '/\/\/BEGIN MODULEBUILDER LINES.*?\/\/END MODULEBUILDER LINES\s*/s';
foreach ($TFilePaths as $filePath) {
if (! removePatternFromFile($filePath, $pattern)) {
// Skip files that were not generated (e.g. the API class when API generation is disabled);
// a missing optional file must not abort the whole object generation.
if (file_exists($filePath) && !removePatternFromFile($filePath, $pattern)) {
$error++;
}
}
}
}
// Apply object tab selection on the generated lib file:
// selected tabs -> hardcode the show flag to 1 (visible without extra config) ; unselected -> remove flag declaration and tab block
if (!$error) {
$libdestfile = $destdir.'/'.$ncObj->applyToFilename('lib/mymodule_myobject.lib.php');
foreach (getModuleBuilderObjectTabs() as $tabkey => $tabinfo) {
$marker = $tabinfo['marker'];
if (in_array($tabkey, $enabledtabs, true)) {
$arrayreplacement = array(
'/\$'.$tabinfo['var'].' = getDolGlobalInt\([^;]*\);/' => '$'.$tabinfo['var'].' = 1;'
);
if (dolReplaceInFile($libdestfile, $arrayreplacement, '', '0', 0, 1) < 0) {
$error++;
dol_syslog("modulebuilder: failed to activate tab flag '".$tabkey."' in ".$libdestfile, LOG_ERR);
}
} else {
if (!removePatternFromFile($libdestfile, '/\h*\/\/ BEGIN MODULEBUILDER TABFLAG '.$marker.'.*?\/\/ END MODULEBUILDER TABFLAG '.$marker.'\s*/s')
|| !removePatternFromFile($libdestfile, '/\h*\/\/ BEGIN MODULEBUILDER TAB '.$marker.'.*?\/\/ END MODULEBUILDER TAB '.$marker.'\s*/s')) {
$error++;
dol_syslog("modulebuilder: failed to purge tab '".$tabkey."' in ".$libdestfile, LOG_ERR);
}
}
}
// Agenda has an extra event widget on the card page: purge it too to avoid a dead link when the agenda tab is excluded
if (!$error && !in_array('agenda', $enabledtabs, true)) {
$carddestfile = $destdir.'/'.$ncObj->applyToFilename('myobject_card.php');
if (!removePatternFromFile($carddestfile, '/\h*\/\/ BEGIN MODULEBUILDER TAB AGENDA.*?\/\/ END MODULEBUILDER TAB AGENDA\s*/s')) {
$error++;
dol_syslog("modulebuilder: failed to purge agenda widget in ".$carddestfile, LOG_ERR);
}
}
if ($objectalreadyexists) {
setEventMessages($langs->trans("WarningTabSelectionOnRegeneration"), null, 'warnings');
}
}
if (!$error) {
// Edit PHP files to make replacement
foreach ($filetogenerate as $destfile) {
@ -1752,7 +1800,12 @@ if ($dirins && $action == 'initobject' && $module && $objectname) { // Test on
]
);
$result = dolReplaceInFile($phpfileval['fullname'], $arrayreplacement); // @phpstan-ignore-line
if (basename($phpfileval['fullname']) === 'mod'.$module.'.class.php') {
// Module descriptor: substitute content but keep the persistent MODULEBUILDER markers intact
$result = dolReplaceInFilePreservingModuleBuilderMarkers($phpfileval['fullname'], $arrayreplacement);
} else {
$result = dolReplaceInFile($phpfileval['fullname'], $arrayreplacement); // @phpstan-ignore-line
}
//var_dump($result);
if ($result < 0) {
setEventMessages($langs->trans("ErrorFailToMakeReplacementInto", $phpfileval['fullname']), null, 'errors');
@ -1778,8 +1831,15 @@ if ($dirins && $action == 'initobject' && $module && $objectname) { // Test on
]
);
$allModulePhpFiles = dol_dir_list($destdir, 'files', 1, '\.php$');
// The module descriptor must NOT go through the blanket substitution: it keeps persistent
// MYOBJECT/MYMODULE markers (TOPMENU/LEFTMENU) reused when generating subsequent objects, and its
// own placeholders are already resolved by initmodule and the dedicated menu/permission blocks.
$moduledescriptorbasename = 'mod'.$module.'.class.php';
if (is_array($allModulePhpFiles) && !empty($allModulePhpFiles)) {
foreach ($allModulePhpFiles as $phpFileval) {
if (basename($phpFileval['fullname']) === $moduledescriptorbasename) {
continue;
}
$result = dolReplaceInFile($phpFileval['fullname'], $moduleReplacementAll);
if ($result < 0) {
setEventMessages($langs->trans("ErrorFailToMakeReplacementInto", $phpFileval['fullname']), null, 'warnings');
@ -4234,6 +4294,9 @@ if ($module == 'initmodule') {
print '<input type="hidden" name="tab" value="objects">';
print '<input type="hidden" name="module" value="'.dol_escape_htmltag($module).'">';
// Tabs selected by default = all optional tabs; reflect posted state on redisplay
$enabledtabsdefault = GETPOSTISSET('enabledtab') ? GETPOST('enabledtab', 'array') : array_keys(getModuleBuilderObjectTabs());
print '<span class="opacitymedium">'.$langs->trans("EnterNameOfObjectDesc").'</span><br><br>';
print '<div class="tagtable">';
@ -4273,6 +4336,13 @@ if ($module == 'initmodule') {
print '<input type="checkbox" name="includedocgeneration" id="includedocgeneration" value="includedocgeneration"> <label for="includedocgeneration">'.$form->textwithpicto($langs->trans("IncludeDocGeneration"), $langs->trans("IncludeDocGenerationHelp")).'</label><br>';
print '<input type="checkbox" name="generatepermissions" id="generatepermissions" value="generatepermissions"> <label for="generatepermissions">'.$form->textwithpicto($langs->trans("GeneratePermissions"), $langs->trans("GeneratePermissionsHelp")).'</label><br>';
print '<input type="checkbox" name="nogeneratelines" id="nogeneratelines" value="nogeneratelines"> <label for="nogeneratelines">'.$form->textwithpicto($langs->trans("NoGenerateLines"), $langs->trans("NoGenerateLinesHelp")).'</label><br>';
print '<br><span class="opacitymedium">'.$form->textwithpicto($langs->trans("EnabledTabsForObject"), $langs->trans("EnabledTabsForObjectHelp")).'</span><br>';
foreach (getModuleBuilderObjectTabs() as $tabkey => $tabinfo) {
$checked = in_array($tabkey, $enabledtabsdefault, true) ? ' checked' : '';
print '<input type="checkbox" name="enabledtab[]" id="enabledtab_'.$tabkey.'" value="'.dol_escape_htmltag($tabkey).'"'.$checked.'> ';
print '<label for="enabledtab_'.$tabkey.'">'.dol_escape_htmltag($langs->trans($tabinfo['label'])).'</label> &nbsp; ';
}
print '<br>';
print '<br>';
print '<input type="submit" class="button small" name="create" value="'.dol_escape_htmltag($langs->trans("GenerateCode")).'"'.($dirins ? '' : ' disabled="disabled"').'>';
print '<br>';

View file

@ -34,10 +34,18 @@ function myobjectPrepareHead($object)
$langs->load("mymodule@mymodule");
// BEGIN MODULEBUILDER TABFLAG CONTACT
$showtabofpagecontact = getDolGlobalInt('MAIN_MYMODULE_SHOW_PAGE_OF_CONTACT');
// END MODULEBUILDER TABFLAG CONTACT
// BEGIN MODULEBUILDER TABFLAG NOTE
$showtabofpagenote = getDolGlobalInt('MAIN_MYMODULE_SHOW_PAGE_OF_NOTE');
// END MODULEBUILDER TABFLAG NOTE
// BEGIN MODULEBUILDER TABFLAG DOCUMENT
$showtabofpagedocument = getDolGlobalInt('MAIN_MYMODULE_SHOW_PAGE_OF_DOCUMENT');
// END MODULEBUILDER TABFLAG DOCUMENT
// BEGIN MODULEBUILDER TABFLAG AGENDA
$showtabofpageagenda = getDolGlobalInt('MAIN_MYMODULE_SHOW_PAGE_OF_AGENDA');
// END MODULEBUILDER TABFLAG AGENDA
$h = 0;
$head = array();
@ -47,13 +55,16 @@ function myobjectPrepareHead($object)
$head[$h][2] = 'card';
$h++;
// BEGIN MODULEBUILDER TAB CONTACT
if ($showtabofpagecontact) {
$head[$h][0] = dolBuildUrl(dol_buildpath("/mymodule/myobject_contact.php", 1), ['id' => $object->id]);
$head[$h][1] = $langs->trans("Contacts");
$head[$h][2] = 'contact';
$h++;
}
// END MODULEBUILDER TAB CONTACT
// BEGIN MODULEBUILDER TAB NOTE
if ($showtabofpagenote) {
if (isset($object->fields['note_public']) || isset($object->fields['note_private'])) {
$nbNote = 0;
@ -72,7 +83,9 @@ function myobjectPrepareHead($object)
$h++;
}
}
// END MODULEBUILDER TAB NOTE
// BEGIN MODULEBUILDER TAB DOCUMENT
if ($showtabofpagedocument) {
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/link.class.php';
@ -87,13 +100,16 @@ function myobjectPrepareHead($object)
$head[$h][2] = 'document';
$h++;
}
// END MODULEBUILDER TAB DOCUMENT
// BEGIN MODULEBUILDER TAB AGENDA
if ($showtabofpageagenda) {
$head[$h][0] = dolBuildUrl(dol_buildpath("/mymodule/myobject_agenda.php", 1), ['id' => $object->id]);
$head[$h][1] = $langs->trans("Events");
$head[$h][2] = 'agenda';
$h++;
}
// END MODULEBUILDER TAB AGENDA
// Show more tabs from modules
// Entries must be declared in modules descriptor with line

View file

@ -637,6 +637,7 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
print '</div><div class="fichehalfright">';
// BEGIN MODULEBUILDER TAB AGENDA
$MAXEVENT = 10;
$morehtmlcenter = dolGetButtonTitle($langs->trans('SeeAll'), '', 'fa fa-bars imgforviewmode', dol_buildpath('/mymodule/myobject_agenda.php', 1).'?id='.$object->id);
@ -649,6 +650,7 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
$formactions = new FormActions($db);
$somethingshown = $formactions->showactions($object, $object->element.'@'.$object->module, (is_object($object->thirdparty) ? $object->thirdparty->id : 0), 1, '', $MAXEVENT, '', $morehtmlcenter);
}
// END MODULEBUILDER TAB AGENDA
print '</div></div>';
}

View file

@ -0,0 +1,79 @@
<?php
/* Copyright (C) 2026 ATM Consulting <contact@atm-consulting.fr>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (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/>.
*/
/**
* \file test/phpunit/ModuleBuilderLibTest.php
* \ingroup test
* \brief PHPUnit test for modulebuilder.lib.php tab selection helpers
* \remarks To run this script as CLI: phpunit filename.php
*/
global $conf,$user,$langs,$db;
require_once dirname(__FILE__).'/../../htdocs/master.inc.php';
require_once dirname(__FILE__).'/../../htdocs/core/lib/modulebuilder.lib.php';
require_once dirname(__FILE__).'/CommonClassTest.class.php';
/**
* Class for PHPUnit tests
*
* @backupGlobals disabled
* @backupStaticAttributes enabled
* @remarks backupGlobals must be disabled to have db,conf,user and lang not erased.
* @phan-file-suppress PhanUndeclaredClass
* @phan-file-suppress PhanUndeclaredExtendedClass
* @phan-file-suppress PhanUndeclaredMethod
* @phan-file-suppress PhanTypeMismatchArgumentProbablyReal
*/
class ModuleBuilderLibTest extends CommonClassTest
{
/**
* testGetModuleBuilderObjectTabs
*
* @return void
*/
public function testGetModuleBuilderObjectTabs()
{
$map = getModuleBuilderObjectTabs();
$this->assertSame(array('contact', 'note', 'document', 'agenda'), array_keys($map));
$this->assertSame('myobject_contact.php', $map['contact']['file']);
$this->assertSame('showtabofpageagenda', $map['agenda']['var']);
$this->assertSame('DOCUMENT', $map['document']['marker']);
}
/**
* testFilterEnabledTabs
*
* @return void
*/
public function testFilterEnabledTabs()
{
$map = getModuleBuilderObjectTabs();
// Nominal: returns requested keys in map order
$this->assertSame(array('contact', 'agenda'), filterEnabledTabs(array('agenda', 'contact'), $map));
// Unknown key is rejected
$this->assertSame(array('contact'), filterEnabledTabs(array('contact', 'evil'), $map));
// Empty / non-array returns empty
$this->assertSame(array(), filterEnabledTabs(array(), $map));
$this->assertSame(array(), filterEnabledTabs('', $map));
// Duplicates collapsed
$this->assertSame(array('note'), filterEnabledTabs(array('note', 'note'), $map));
}
}