dolibarr/htdocs/modulebuilder/template/lib/mymodule_myobject.lib.php
VIAL-GOUTEYRON Quentin 3216098ab5
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>
2026-06-09 19:04:00 +02:00

127 lines
4.5 KiB
PHP

<?php
/* Copyright (C) ---Replace with your own copyright and developer email---
* Copyright (C) 2025 Frédéric France <frederic.france@free.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 htdocs/modulebuilder/template/lib/mymodule_myobject.lib.php
* \ingroup mymodule
* \brief Library files with common functions for MyObject
*/
/**
* Prepare array of tabs for MyObject
*
* @param MyObject $object MyObject
* @return array<array{string,string,string}> Array of tabs
*/
function myobjectPrepareHead($object)
{
global $db, $langs, $conf;
$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();
$head[$h][0] = dolBuildUrl(dol_buildpath("/mymodule/myobject_card.php", 1), ['id' => $object->id]);
$head[$h][1] = $langs->trans("MyObject");
$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;
if (!empty($object->note_private)) {
$nbNote++;
}
if (!empty($object->note_public)) {
$nbNote++;
}
$head[$h][0] = dolBuildUrl(dol_buildpath('/mymodule/myobject_note.php', 1), ['id' => $object->id]);
$head[$h][1] = $langs->trans('Notes');
if ($nbNote > 0) {
$head[$h][1] .= (!getDolGlobalInt('MAIN_OPTIMIZEFORTEXTBROWSER') ? '<span class="badge marginleftonlyshort">'.$nbNote.'</span>' : '');
}
$head[$h][2] = 'note';
$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';
$upload_dir = $conf->mymodule->dir_output."/myobject/".dol_sanitizeFileName($object->ref);
$nbFiles = count(dol_dir_list($upload_dir, 'files', 0, '', '(\.meta|_preview.*\.png)$'));
$nbLinks = Link::count($db, $object->element, $object->id);
$head[$h][0] = dolBuildUrl(dol_buildpath("/mymodule/myobject_document.php", 1), ['id' => $object->id]);
$head[$h][1] = $langs->trans('Documents');
if (($nbFiles + $nbLinks) > 0) {
$head[$h][1] .= '<span class="badge marginleftonlyshort">'.($nbFiles + $nbLinks).'</span>';
}
$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
//$this->tabs = array(
// 'entity:+tabname:Title:@mymodule:/mymodule/mypage.php?id=__ID__'
//); // to add new tab
//$this->tabs = array(
// 'entity:-tabname:Title:@mymodule:/mymodule/mypage.php?id=__ID__'
//); // to remove a tab
complete_head_from_modules($conf, $langs, $object, $head, $h, 'myobject@mymodule');
complete_head_from_modules($conf, $langs, $object, $head, $h, 'myobject@mymodule', 'remove');
return $head;
}