Skill: Creare campi (Field) di master-core
Regola d'oro: un campo NON è HTML statico. È una classe PHP registrata via
$form->addField('<Tipo>', [config])inconfig.php. Il nome del campo decide dove viene salvato il valore. Il tipo decide come si renderizza, valida e riempie il model. Non scrivere mai<input>a mano in una view admin: aggiungi un Field.
Validazione — scripts dedicati (scripts/), non a memoria
Questa skill ha due parti distinte: una guida concettuale (come funziona il sistema dei Field — sotto) e una validazione meccanica fatta da script deterministici, non da giudizio dell'AI. La sintassi obbligatoria di un Field custom (estende la base giusta, ha render(), namespace coerente con la posizione, nome classe = nome file) e la regola "0 JS/CSS inline" non si controllano leggendo il codice a occhio: le verifica uno script, sola lettura, richiamabile da CLI.
| Script | Cosa verifica |
|---|---|
scripts/check-field-class-structure.php <path> |
Un Field custom estende Field/BaseField; dichiara render(); il namespace combacia con la cartella (Foundation/Form/Fields → Master\Foundation\Form\Fields, Modules/<Nome>/Form/Fields → Master\Modules\<Nome>\Form\Fields); il nome della classe combacia col nome del file |
scripts/check-field-no-inline-assets.php <path> |
Nessun <script>, <style>, onclick=, style= dentro un Field custom (§7bis) |
Uso: php scripts/<nome>.php <path-file-o-cartella>. Output JSON su stdout: { script, target, files_checked, findings: [{rule, severity, file, line, message, confidence?}] }. Come negli script di master-code-review: euristiche a regex, non un parser PHP/AST — su casi ambigui emettono confidence: "da_verificare" invece di affermare un errore.
Quando eseguirli: sempre dopo aver creato o modificato un Field custom (§7), prima di considerarlo finito. Esegui entrambi gli script sulla cartella Form/Fields/ interessata, traduci ogni riga di findings in un problema da correggere — non ri-derivare a mano ciò che lo script ha già stabilito.
Nota:
master-code-reviewha script propri che si sovrappongono parzialmente a questi (check-field-antipatterns.phpcopre anche JS/CSS inline nei Field custom,check-field-destinations.phpcopre il mapping name→destinazione). La deduplicazione fra le due skill è una decisione ancora da prendere — per ora entrambi gli insiemi di script sono validi e indipendenti.
Il catalogo dei tipi (§5) può essere obsoleto — verifica sempre sulla fonte
La tabella dei tipi in §5 è una guida, non la fonte di verità: nuovi tipi possono essere aggiunti al package o a Master\Foundation\Form\Fields senza che questo file venga aggiornato. Se stai per usare un tipo che non ricordi con certezza, o vuoi essere sicuro dell'elenco completo e aggiornato, non fidarti della tabella a memoria: elenca dal vivo private/vendor/enesisrl/laravel-master-core/src/Foundation/Form/Fields/*.php (+ eventuali override in private/master/Foundation/Form/Fields/*.php) e leggi la classe stessa per la config supportata. La tabella serve per orientarsi velocemente, la lettura del sorgente è quella che decide.
0. Prima di iniziare: dove vivono i campi
- I campi si dichiarano nel
config.phpdel modulo:private/master/Modules/<Nome>/config.php, dentrocrud.form = function($form, $model){ ... }. - Le classi base dei tipi stanno nel package (NON modificarle):
private/vendor/enesisrl/laravel-master-core/src/Foundation/Form/Fields/*.phpe la classe astrattaFoundation/Form/Field.php+ l'orchestratoreFoundation/Form/Form.php. - I campi custom si possono definire a due livelli (vedi §7):
- Globale di progetto:
private/master/Foundation/Form/Fields/(namespaceMaster\Foundation\Form\Fields) — risolto per nome breve. - Locale al modulo:
private/master/Modules/<ModuleName>/Form/Fields/<FieldName>.php(namespaceMaster\Modules\<ModuleName>\Form\Fields) — risolto passando l'FQCN.
- Globale di progetto:
addField($class, $config) risolve la classe così (Form::addField):
\Master\Foundation\Form\Fields\{$class}(nome breve → override/globale di progetto) — ha priorità;- altrimenti
{$class}interpretato come FQCN completo (es. classe locale al modulo); - deve estendere
Field, altrimentiRuntimeException.
Quindi:
'Varchar'(stringa breve) → tipo del package, salvo che esistaMaster\Foundation\Form\Fields\Varchar.Master\Modules\Roles\Form\Fields\Permissions::class(FQCN) → campo definito dentro il modulo.
Esempio reale — Modules/Roles/config.php:
use Master\Modules\Roles\Form\Fields\Permissions as PermissionsField;
// ...
$form->addField(PermissionsField::class, [ 'name' => 'permissions', /* ... */ ]);
Con la classe in private/master/Modules/Roles/Form/Fields/Permissions.php e namespace Master\Modules\Roles\Form\Fields.
1. Modello mentale: il ciclo di vita di un Field
Ogni Field attraversa queste fasi (definite in Field.php, orchestrate da Form.php):
| Fase | Metodo | Cosa fa |
|---|---|---|
| Costruzione | __construct → register() |
riceve la config; register() (override) può costruire sotto-campi |
| Lettura valore | getValue() / getValueLang($lang) |
legge dal model (colonna, traduzione, tabella valori, indirizzo) |
| Render form | render() |
produce l'HTML dell'input |
| Render sola lettura | renderViewMode() |
HTML in viewMode (default: label + valore) |
| Validazione | modifyValidator() → getValidatorRules() |
applica rules al Validator |
| Salvataggio | fillModel() |
scrive getRequestValue() sul model |
Il salvataggio finale su DB (traduzioni, tabella valori, indirizzi) NON lo fa il Field: lo fa il model in saveData() (base Model.php), leggendo la Request per nome campo. Il Field deve solo produrre un name HTML coerente con la convenzione (§2).
2. LA COSA PIÙ IMPORTANTE: il name decide dove va il valore
Il name del campo NON è arbitrario. È il contratto con saveData():
Se name = ... |
Il valore finisce in ... | Condizione |
|---|---|---|
sequence, published, date_from (colonna diretta) |
colonna della tabella principale | il nome è nel $fillable del model |
description__it, meta_title__en (auto-generato dai campi *Lang) |
tabella <entity>_translations (colonna description, riga lang=it) |
il nome base è nel $fillable del Translation model |
project_category_id |
tabella <entity>_values (field_name/field_value) |
il nome è nell'array protected $related del model |
cover, image |
Spatie MediaLibrary (collection omonima) | esiste registerMediaCollections() con quella collection |
address_id |
tabella indirizzi | il model usa HasAddresses e lo dichiara in initializeHasAddresses() |
Conseguenza pratica: aggiungere un campo spesso richiede più del solo addField. Prima di dichiarare il campo, verifica che la destinazione esista:
- Campo colonna diretta → aggiungi la colonna nella migration e mettila nel
$fillabledel model. - Campo multilingua (
*Lang) → la colonna base deve esistere nella tabella_translationse nel$fillabledel Translation model. NON serve la colonnanome__it:saveData()fa il match sucampo__lang. - Campo multi-valore (
Select multiple, categorie…) → aggiungi il nome all'arrayprotected $relateddel model; i valori vanno in<entity>_values. - Media → aggiungi la collection in
registerMediaCollections()(+ eventuali conversioni). - Indirizzo → trait
HasAddresses+initializeHasAddresses().
Se salvi un campo e "sparisce", quasi sempre è perché la destinazione non è dichiarata: non è nel
$fillable, non è nel Translation$fillable, o manca da$related.
3. Anatomia di addField + chiavi di config comuni
$form->addField('Varchar', [
'name' => 'phone', // OBBLIGATORIO — vedi §2
'label' => __('admin::label.phone'), // etichetta (usa le lang key admin::)
'rules' => ['nullable', 'max:50'], // regole Laravel Validator (array o closure)
'help' => __('admin::help.phone'), // testo sotto l'input
'placeholder' => '+39 ...',
'customCssClasses' => 'text-uppercase', // classi extra sull'input
'readonly' => true, // solo lettura
'disabled' => true, // disabilitato (invia hidden col valore)
]);
Chiavi valide su (quasi) tutti i campi — lette via $this->config(...):
name(obbligatorio),label,help,rules,placeholder,customCssClasses.maxlength→ attributodata-admin-maxlength+ regola implicita in alcuni campi Lang.readonly,disabled(dove supportati).field_name→ nome colonna DB reale se diverso daname(usato perenum,Date, list).custom_value→ forza un valore fisso, scavalca il model.sessionValue→ valore preso dalla sessione (es. filtri di ricerca).displayValue→ valore mostrato in viewMode; può essere una closurefn($value) => ....lang_source→'admin'usa le lingue admin, altrimenti le lingue front (per i campi multilingua).viewMode→ forza la sola lettura.
Layout: dopo aver aggiunto i campi, li disponi in tab/righe con addTab, referenziando i campi per name con la sintassi nome|col:6 (griglia Bootstrap a 12), più pseudo-elementi :separator, :section-title|title:..., :section-subtitle|title:....
$form->addTab([
'label' => __('admin::label.informazioni_generali'),
'content' => [
['published|col:2', 'sequence|col:2', 'date_from|col:4', 'date_to|col:4'],
[':separator'],
['description|col:12'],
],
]);
Un campo aggiunto con addField ma non referenziato in nessun tab non viene renderizzato (ma esiste comunque per validazione/salvataggio se presente nella Request).
4. rules: validazione
rules accetta:
- un array di regole Laravel:
['required', 'max:255', 'nullable', 'date', 'after_or_equal:date_from']; - una closure che riceve il model:
'rules' => fn($model) => $model->id ? ['nullable'] : ['required'].
Le regole vengono unite nel Validator del form (Form::getValidator). I messaggi arrivano da admin::validation. La label del campo viene registrata come attributo custom, quindi i messaggi usano la label giusta.
Per i campi multilingua (*Lang) le regole required/maxlength vengono propagate a ogni sotto-campo lingua automaticamente (vedi TextLang::register).
5. Catalogo dei tipi disponibili (package)
Tipi in Foundation/Form/Fields/. Colonna "Salva dove" = comportamento tipico in base al name (§2).
Per i tipi con comportamento complesso esiste un file satellite in
references/con dettagli, esempi reali dai moduli e insidie note (caricato solo quando serve, non ingombra questo file):select.md,crud.md,medialibrary.md.
Testo / input semplici
| Tipo | Uso | Config specifica |
|---|---|---|
Varchar |
input testo <input type=text> |
numbersonly, readonly, disable_on_edit, search_type |
Text |
<textarea> |
editor (es. summernote) + editorOptions (JSON), maxlength |
Email / Phone / Password / Color |
varianti di input tipizzato | come Varchar |
Integer / Decimal / Currency |
numerici | Currency: currency_symbol (euro|dollars|custom+symbol_class) |
Hidden |
input nascosto | value, data-attrs |
Value |
hidden + titolo visibile | displayValue |
Autocomplete |
typeahead | autocomplete_options (JSON), readonly |
Multilingua (generano un sotto-campo per lingua: name__it, name__en, …)
| Tipo | Base per lingua |
|---|---|
VarcharLang |
Varchar |
TextLang |
Text (supporta editor) |
SlugLang |
Slug (supporta input_src) |
TagLang |
Tag |
ComboBoxLang |
ComboBox (option filtrate per lang) |
I
*Langcostruiscono internamente unanew Forminregister()e delegano render/validate/fillModel. NON aggiungere manualmente i suffissi__it: ci pensa il campo.
Scelte
| Tipo | Uso | Config specifica |
|---|---|---|
Select |
dropdown | type: standard (con query SQL), values (con resultSet), enum (con table+field_name), ajax (con ajaxCallClass); multiple+multiple-options; select2; readonly; disabled. Dettagli, esempi reali e insidie → references/select.md. |
ComboBox |
select con ricerca (data-admin-combobox) |
type=values + resultSet (opz. per lang) |
Radio |
radio button | type come Select; media (mostra immagini) |
Checkbox |
singola checkbox | checkbox_value (default 1) |
Published |
checkbox "attivo" con default a 1 | checkbox_value |
resultSet è un array di ['value'=>..., 'description'=>...] (per ComboBoxLang anche 'lang'=>...). Se manca description usa admin::option.<value>.
Date / tempo
| Tipo | Uso | Config |
|---|---|---|
Date |
date picker | type: date (DD-MM-YYYY) o datetime (DD-MM-YYYY HH:mm) — converte in Y-m-d in getRequestValue() |
Daterange |
intervallo | intervallo di date |
Orario / Orari |
orari (giornalieri/settimanali) | strutture orario |
Media / file
| Tipo | Uso | Config |
|---|---|---|
MediaLibrary |
upload Spatie MediaLibrary | maxNumberOfFiles (default 1), allowedFileTypes (['jpg','png','webp',...]), maxFileSize, disableEditImage. Richiede la collection in registerMediaCollections(). L'upload è possibile solo dopo il primo salvataggio (serve un id). Dettagli, esempi reali e insidie → references/medialibrary.md. |
File / Image |
upload semplice | tipi file |
Speciali / strutturali
| Tipo | Uso | Config |
|---|---|---|
Sequence |
ordinamento numerico con auto-next | min, max, step, table — calcola il prossimo valore (+10) se nuovo/draft |
Slug |
slug con generazione da altro campo | input_src (campo sorgente), table, foreign_key |
Address |
indirizzo (package addresses) | required; richiede HasAddresses sul model |
Coords |
latitudine/longitudine | coordinate |
Tag |
tag input | tag |
Contents |
blocchi di contenuto (page_contents) | moduleName, contentConfigs, viewsModuleName. Vedi skill master-page-content. |
Crud |
tabella di un sotto-modulo collegato inline | module, reference_key. Funziona solo dopo il salvataggio (serve id). Dettagli, esempi reali e insidie → references/crud.md. |
Button |
pulsante azione | azione custom |
Prima di inventare un tipo, controlla la cartella
Fields/del package: c'è quasi sempre già il tipo giusto.
6. Comportamenti speciali da tenere a mente
Casi in cui il Field NON si comporta come un banale input:
- Campi
*Lang— non hanno una colonna propria; generano N sotto-campiname__<lang>e delegano tutto a una sotto-Form. La destinazione è la tabella_translations. Select/Radiomultiple +related— conmultiple => trueil name diventaname[]e i valori vanno nella tabella_values. Il model DEVE avere il campo inprotected $related, altrimentisaveData()li ignora.Sequence— in creazione o su recorddraftcalcola automaticamente il prossimo valore; rispetta anche il filtroparent(sotto-moduli).Date— mostraDD-MM-YYYYma salvaY-m-d(override digetValue/getRequestValue). Non passare formati diversi.Published— default a 1 se il valore ènull(nuovo record nasce "attivo").Checkbox/Published— se non spuntati NON inviano nulla nella Request: il valore resta il default DB / non aggiornato. Tienine conto nelle regole.MediaLibrary/Crud— richiedono unidesistente: in creazione mostrano un messaggio "salva prima". Vanno tipicamente in un tab separato o dopo il primo save.Value/Hidden— mostrano/inviano un valore ma spesso servono a portare in Request dati calcolati (custom_value,displayValue).address_field— un campo può leggere il proprio valore dalla tabella indirizzi seconfig('address_field')è impostato e il model hagetAddress().- viewMode — molti campi hanno un
renderViewMode()dedicato; se crei un campo custom con markup complesso, implementalo o eredita il default (label + valore).
7. Creare un Field CUSTOM
PRIMA di creare un campo, verifica se ne esiste già uno simile — non reinventare la ruota. Controlla nell'ordine: (1) i tipi del package in
Foundation/Form/Fields/(catalogo §5); (2) i campi globali di progetto inMaster\Foundation\Form\Fields; (3) i campi locali agli altri moduli (Modules/*/Form/Fields/). Se ne esiste uno riutilizzabile, usalo.Se serve una piccola variazione, valuta se puoi riutilizzare il campo esistente con modifiche minime (es. una nuova chiave di config opzionale, un ramo aggiuntivo) — a condizione che NON cambino il comportamento per gli altri form che già lo usano. Regole per una modifica sicura: retro-compatibile (nuove config opzionali con default che preservano il comportamento attuale), nessuna rinomina/rimozione di config o attributi esistenti, nessun cambio al
name/output per i casi già in uso. Se la modifica rischia di rompere un altro form, NON toccare il campo condiviso: crea un nuovo campo (o estendi quello esistente in una sottoclasse).
Quando nessun tipo del package basta, scegli dove vive il campo:
- Riusabile in più moduli → globale di progetto:
private/master/Foundation/Form/Fields/<NomeTipo>.php, namespaceMaster\Foundation\Form\Fields. Inconfig.phplo usi col nome breve:$form->addField('ColorSwatch', [...]). - Specifico di un solo modulo → locale al modulo:
private/master/Modules/<ModuleName>/Form/Fields/<NomeTipo>.php, namespaceMaster\Modules\<ModuleName>\Form\Fields. Inconfig.phplo usi con l'FQCN:$form->addField(\Master\Modules\<ModuleName>\Form\Fields\<NomeTipo>::class, [...])(esempio reale:Modules/Roles/Form/Fields/Permissions.php).
In entrambi i casi la classe estende BaseField ed è identica; cambia solo namespace/percorso e come la referenzi in addField. Esempio (versione globale):
<?php
namespace Master\Foundation\Form\Fields;
use Master\Foundation\Form\Field as BaseField;
class ColorSwatch extends BaseField
{
// (opz.) costruisci stato/sotto-campi una sola volta
protected function register(): void
{
// es. leggere config, preparare opzioni
}
// OBBLIGATORIO: HTML dell'input. Usa SEMPRE getName()/getLabel()/getValue()/getHelp().
public function render(): string
{
$value = ($this->config('valueLang'))
? $this->getValueLang($this->config('valueLang'))
: $this->getValue();
return '<div class="form-group">'
. '<label>' . $this->getLabel() . '</label>'
. '<input type="color" class="form-control ' . $this->config('customCssClasses') . '"'
. ' value="' . htmlspecialchars($value, ENT_QUOTES) . '"'
. ' name="' . $this->getName() . '" id="' . $this->getName() . '_field">'
. $this->getHelp()
. '</div>';
}
}
Poi in config.php: $form->addField('ColorSwatch', ['name' => 'brand_color', 'label' => '...']);
Metodi che puoi (ri)definire — vedi Field.php per le firme:
render()— quasi sempre necessario; markup dell'input.renderViewMode()— HTML in sola lettura (default: label + valore, con link se URL).getValue()/getValueLang($lang)— override per trasformare il valore letto dal model (es.Dateformatta;Selectrisolve la description in viewMode).getRequestValue()— override per trasformare l'input prima del salvataggio (es.Date→Y-m-d).modifyValidator($validator)— override solo per regole particolari; il default applicaconfig('rules').fillModel()— override solo se il valore non va su una colonna diretta (es. i*Langdelegano alla sotto-Form). Il default fasetAttributeIfExists(name, requestValue).register()— per campi compositi (crea unanew Forme aggiungi sotto-campi con namename__<lang>o simili).
Convenzioni per un Field custom corretto:
- Escapa sempre i valori in output (
htmlspecialchars(..., ENT_QUOTES)), come fannoVarchar/Autocomplete. - Rispetta
customCssClasses,help,placeholder,readonly,disabledse ha senso. - Il markup admin è KTMetronic/Bootstrap 4: wrap in
.form-group,<label>,.form-control. - Se il campo è multilingua, imita
TextLang/SlugLang: costruisci una sotto-Forminregister()e delegarender/modifyValidator/fillModel. - Per valori NON su colonna diretta (traduzioni, valori, media) fai in modo che il
namecombaci con la convenzione (§2) e, se necessario, overridefillModel(). - Mai JS/CSS dentro
render()(niente<script>,<style>,onclick=,style=): JS intheme.js/pluginAdmin, CSS intheme.csso view Blade del modulo — vedi §7bis. - NON modificare mai i file sotto
vendor/. Un override si fa creando la classe omonima inMaster\Foundation\Form\Fields.
7bis. JS e CSS dei campi: DOVE metterli (MAI nel form)
Regola assoluta: il JavaScript non va MAI dentro il form / dentro
render()del campo. Niente<script>inline, nienteonclick=nel markup del Field. Idem per il CSS: niente<style>inline.
JavaScript
Due destinazioni possibili:
assets/master/js/theme.js— JS globale dell'admin di progetto (crea il file se non esiste; vive accanto aadmin.js/custom.jsinassets/master/js/).- Una view Blade dedicata nel modulo (es.
Modules/<Nome>/Views/.../scripts.blade.php) inclusa nella pagina admin — per JS specifico di quel modulo.
Usiamo jQuery. Esiste inoltre la libreria Admin (assets/master/js/admin.js) che gestisce il ciclo di vita dei plugin interni. Decisione da prendere caso per caso (spetta a chi sviluppa):
- Intervento una tantum / specifico → jQuery semplice in
theme.jso nellascripts.blade.phpdel modulo. - Comportamento riusabile legato a un campo → conviene trasformarlo in un plugin
Admin:- ⚠️ PRIMA di scrivere un plugin
Admin, consulta la documentazione su Enesi Academy per l'uso corretto delle funzioni diadmin.js(Admin.plug,Admin.load,Admin.on/trigger, ecc.): usasearch_academy/get_docdel MCP Enesi Academy. Questo passaggio serve solo quando stai creando un pluginAdmininsieme al campo, non per un intervento jQuery una tantum. - il Field renderizza un attributo
data-admin-<nome>sull'input (come fanno i campi del package:data-admin-select2,data-admin-combobox,data-admin-slug,data-admin-datetimepicker,data-admin-media-library-input, …); - registri il plugin con
Admin.plug('<nome>', function(context){ $(context).find('[data-admin-<nome>]').each(...) }); Admin.load(context)(ri)inizializza i plugin in un contesto — fondamentale: l'admin ricarica pezzi via AJAX (tab, tabelle CRUD, modali) e richiamaAdmin.load()sul nuovo DOM. Un plugin fatto così si re-inizializza da solo dopo ogni reload; una$(document).readyuna tantum no.- eventi/azioni disponibili:
Admin.on/one/off/trigger,Admin.ajax,Admin.service.
- ⚠️ PRIMA di scrivere un plugin
Perché è importante per i campi: se il tuo Field custom aggancia JS con un
ready()una tantum, dopo un reload AJAX del tab/della tabella CRUD il comportamento sparisce. Condata-admin-*+Admin.plugfunziona sempre.
CSS
Due destinazioni:
assets/master/css/theme.css— CSS globale dell'admin di progetto (crea il file se non esiste; accanto acustom.css).- Una view Blade dedicata nel modulo — per stili specifici del modulo.
Mai style="..." inline o <style> dentro render().
8. Checklist per "aggiungere un campo"
- Esiste già un campo simile? Cerca in package → globali di progetto → altri moduli. Riusalo se possibile; se basta una modifica minima e retro-compatibile a un campo esistente (senza rompere gli altri form che lo usano), procedi così invece di crearne uno nuovo (§7).
- Che dato è? colonna diretta / multilingua / multi-valore / media / indirizzo / blocchi. → decide il tipo e la destinazione (§2).
- Esiste la destinazione? migration +
$fillable(o Translation$fillable, o$related, o media collection, oHasAddresses). - Scegli il tipo dal catalogo (§5). Multilingua → usa la variante
*Lang. $form->addField('<Tipo>', ['name'=>..., 'label'=>..., 'rules'=>[...]]).- Referenzia il campo in un
addTab(... 'nome|col:X' ...), altrimenti non appare. - Regole coerenti (ricorda: checkbox non spuntate non arrivano in Request).
- Se serve un comportamento non coperto → Field custom in
Master\Foundation\Form\Fields(§7). Fatto il Field, eseguiscripts/check-field-class-structure.phpescripts/check-field-no-inline-assets.phpsulla cartellaForm/Fields/— non considerarlo finito finché non tornano puliti. - Svuota le cache admin se non vedi il campo:
php artisan config:clear && php artisan view:clear(daprivate/).
Riferimenti nel codice
- Classe base:
private/vendor/enesisrl/laravel-master-core/src/Foundation/Form/Field.php - Orchestratore +
addField/layout/validazione:.../Foundation/Form/Form.php - Tutti i tipi:
.../Foundation/Form/Fields/*.php - Salvataggio (traduzioni/valori/indirizzi):
.../Foundation/Modules/Base/Models/Model.php(saveData) - Esempi reali completi:
private/master/Modules/Projects/config.php,.../Pages/config.php - Model con
$fillable/$related/media/HasAddresses:private/master/Modules/Projects/Models/Project.php - Blocchi di contenuto (
Contents): vedi skillmaster-page-content.