Auf dieser Seite
- Eine Kontaktverwaltung vollständig bauen
- 1. Modul und Routen anlegen
- 2. Vollständige DD schreiben und synchronisieren
- 3. Formular-FD vollständig definieren
- 4. Form-Template anlegen
- 5. Service für Laden, Validieren und Speichern
- 6. Filter-FD und dbxReport ergänzen
- 7. Löschen serverseitig absichern
- 8. Normal- und Ajax-Request vergleichen
- 9. Vertragstest und Abnahme
Eine Kontaktverwaltung vollständig bauen
Sie erstellen ein lauffähiges Modul entlang des verbindlichen Golden Path: DD und DD-Sync, FD und dbxForm, dbxReport, serverseitig geschützte Aktionen, Ajax und Vertragstests.
Das überprüfbare Ergebnis
?dbx_modul=myContactszeigt eine filterbare Kontaktliste.- „Neu“ und „Bearbeiten“ öffnen dasselbe FD-basierte dbxForm.
- Speichern und Löschen laufen ausschließlich über dbxDB und eine vollständige DD.
- Normale und Ajax-Requests verwenden denselben PHP-Codepfad.
myInvoices aus dbxapp 4.5.3. Kopieren Sie die Dateien in eine Entwicklungsinstallation, nicht direkt in ein Produktivsystem.1. Modul und Routen anlegen
Legen Sie diese Struktur an. Router und Service bleiben getrennt; DD und FD sind eigenständige Verträge.
dbx/modules/myContacts/
├── cfg/config.php
├── dd/contact.dd.php
├── fd/contact-form.fd.php
├── fd/contact-report.fd.php
├── include/myContactsService.class.php
├── tpl/htm/contact-form.htm
├── tpl/htm/contact-report.htm
├── tpl/htm/contact-row-action.htm
├── tests/myContactsContract_test.php
└── myContacts.class.php
Datei myContacts.class.php
<?php
namespace dbx\myContacts;
class myContacts
{
public function run(): string
{
$run=(string)dbx()->get_modul_var('dbx_run1','report','parameter|max=32');
$service=dbx()->get_include_obj('myContactsService','myContacts');
return match($run){'form','edit'=>$service->form(),'delete'=>$service->delete(),default=>$service->report()};
}
}
?>
Datei cfg/config.php
<?php
$config['version']='1.0.0';
$config['activ']='1';
$config['groups']='admin';
?>
Jetzt testen: php -l dbx/modules/myContacts/myContacts.class.php
Erwartet: No syntax errors detected. Der Modulaufruf darf bis zum Anlegen des Services noch kontrolliert fehlschlagen.
2. Vollständige DD schreiben und synchronisieren
Die DD enthält Tabelle, jedes Feld und jeden Index im direkt lesbaren Exportformat. Jedes Feld wird einzeln mit demselben vollständigen Schlüsselsatz definiert; Feld-Closures oder eine versteckte Feldfabrik gehören nicht in die DD.
<?php
$table=array(
'server'=>'myContacts|myContacts.db3','table'=>'contact','datadic'=>'contact',
'primary'=>'id','language'=>'0','version'=>'1.0','autosync'=>'1','cache'=>'0',
'trash'=>'0','trace'=>'0','update_sql'=>'','default_sort'=>'name ASC','form-dd-table'=>'',
'read'=>'admin','create'=>'admin','update'=>'admin','delete'=>'admin',
'read_owner'=>'admin,owner','create_owner'=>'admin,owner',
'update_owner'=>'admin,owner','delete_owner'=>'admin,owner'
);
$field['name']='id';$field['type']='int';$field['index']='PRI';$field['length']='11';$field['default']='';
$field['label']='ID';$field['rules']='int';$field['tooltip']='';$field['errormsg']='';$field['placeholder']='';
$field['convert']='';$field['protect']='0';$field['group']='';$field['mask']='';$field['data']='';$field['options']='';
$field['tpl']='hidden';$field['js']='';$field['prompt']='';$fields[]=$field;
$field['name']='create_date';$field['type']='datetime';$field['index']='MUL';$field['length']='-1';$field['default']='';
$field['label']='Erstellt';$field['rules']='datetime';$field['tooltip']='';$field['errormsg']='';$field['placeholder']='';
$field['convert']='date_time';$field['protect']='0';$field['group']='';$field['mask']='';$field['data']='';$field['options']='';
$field['tpl']='hidden';$field['js']='';$field['prompt']='';$fields[]=$field;
$field['name']='create_uid';$field['type']='int';$field['index']='MUL';$field['length']='11';$field['default']='0';
$field['label']='Erstellt von';$field['rules']='int';$field['tooltip']='';$field['errormsg']='';$field['placeholder']='';
$field['convert']='';$field['protect']='0';$field['group']='';$field['mask']='';$field['data']='';$field['options']='';
$field['tpl']='hidden';$field['js']='';$field['prompt']='';$fields[]=$field;
$field['name']='update_date';$field['type']='datetime';$field['index']='MUL';$field['length']='-1';$field['default']='';
$field['label']='Aktualisiert';$field['rules']='datetime';$field['tooltip']='';$field['errormsg']='';$field['placeholder']='';
$field['convert']='date_time';$field['protect']='0';$field['group']='';$field['mask']='';$field['data']='';$field['options']='';
$field['tpl']='hidden';$field['js']='';$field['prompt']='';$fields[]=$field;
$field['name']='update_uid';$field['type']='int';$field['index']='MUL';$field['length']='11';$field['default']='0';
$field['label']='Aktualisiert von';$field['rules']='int';$field['tooltip']='';$field['errormsg']='';$field['placeholder']='';
$field['convert']='';$field['protect']='0';$field['group']='';$field['mask']='';$field['data']='';$field['options']='';
$field['tpl']='hidden';$field['js']='';$field['prompt']='';$fields[]=$field;
$field['name']='owner';$field['type']='int';$field['index']='MUL';$field['length']='11';$field['default']='0';
$field['label']='Owner';$field['rules']='int';$field['tooltip']='';$field['errormsg']='';$field['placeholder']='';
$field['convert']='';$field['protect']='0';$field['group']='';$field['mask']='';$field['data']='';$field['options']='';
$field['tpl']='hidden';$field['js']='';$field['prompt']='';$fields[]=$field;
$field['name']='name';$field['type']='varchar';$field['index']='MUL';$field['length']='160';$field['default']='';
$field['label']='Name';$field['rules']='*|min=2|max=160';$field['tooltip']='';$field['errormsg']='';$field['placeholder']='';
$field['convert']='';$field['protect']='0';$field['group']='';$field['mask']='';$field['data']='';$field['options']='';
$field['tpl']='text-label';$field['js']='';$field['prompt']='';$fields[]=$field;
$field['name']='email';$field['type']='varchar';$field['index']='UNI';$field['length']='190';$field['default']='';
$field['label']='E-Mail';$field['rules']='email|max=190';$field['tooltip']='';$field['errormsg']='';$field['placeholder']='';
$field['convert']='';$field['protect']='0';$field['group']='';$field['mask']='';$field['data']='';$field['options']='';
$field['tpl']='email-label';$field['js']='';$field['prompt']='';$fields[]=$field;
$field['name']='phone';$field['type']='varchar';$field['index']='';$field['length']='40';$field['default']='';
$field['label']='Telefon';$field['rules']='text|max=40';$field['tooltip']='';$field['errormsg']='';$field['placeholder']='';
$field['convert']='';$field['protect']='0';$field['group']='';$field['mask']='';$field['data']='';$field['options']='';
$field['tpl']='text-label';$field['js']='';$field['prompt']='';$fields[]=$field;
$field['name']='status';$field['type']='varchar';$field['index']='MUL';$field['length']='16';$field['default']='active';
$field['label']='Status';$field['rules']='parameter|max=16';$field['tooltip']='';$field['errormsg']='';$field['placeholder']='';
$field['convert']='';$field['protect']='0';$field['group']='';$field['mask']='';$field['data']='';$field['options']='active=Aktiv&blocked=Gesperrt';
$field['tpl']='select-single-label';$field['js']='';$field['prompt']='';$fields[]=$field;
$indexes=array(
array('name'=>'pk_contact','type'=>'PRIMARY','fields'=>'id','unique'=>'1','comment'=>'Primärschlüssel'),
array('name'=>'idx_contact_create_uid','type'=>'INDEX','fields'=>'create_uid','unique'=>'0','comment'=>'Erstellt von'),
array('name'=>'idx_contact_update_uid','type'=>'INDEX','fields'=>'update_uid','unique'=>'0','comment'=>'Aktualisiert von'),
array('name'=>'idx_contact_owner','type'=>'INDEX','fields'=>'owner','unique'=>'0','comment'=>'Eigentümer'),
array('name'=>'idx_contact_name','type'=>'INDEX','fields'=>'name','unique'=>'0','comment'=>'Namenssuche'),
array('name'=>'uidx_contact_email','type'=>'UNIQUE','fields'=>'email','unique'=>'1','comment'=>'E-Mail eindeutig'),
array('name'=>'idx_contact_status','type'=>'INDEX','fields'=>'status','unique'=>'0','comment'=>'Statusfilter')
);
?>
Jetzt testen: Wählen Sie in der DD-Synchronisierung myContacts|contact und führen Sie den Abgleich zweimal aus.
Erwartet: Der erste Lauf legt Tabelle und Indizes an; der zweite meldet keine Strukturänderung. DD- und DB-Feldzahlen stimmen überein.
3. Formular-FD vollständig definieren
Datei fd/contact-form.fd.php
<?php
$messages=array('form_info'=>'Kontaktdaten prüfen und speichern.',
'form_title_new'=>'Neuer Kontakt','form_title_edit'=>'Kontakt bearbeiten',
'not_found'=>'Kontakt nicht gefunden.','validation_error'=>'Bitte Eingaben prüfen.');
$fields=array();
foreach(array(
array('name','varchar','text-label','Name','*|min=2|max=160',''),
array('email','varchar','email-label','E-Mail','email|max=190',''),
array('phone','varchar','text-label','Telefon','text|max=40',''),
array('status','varchar','select-single-label','Status','parameter|max=16','active=Aktiv&blocked=Gesperrt')
)as $d){$fields[]=array('name'=>$d[0],'type'=>$d[1],'tpl'=>$d[2],'label'=>$d[3],
'rules'=>$d[4],'options'=>$d[5],'index'=>'','length'=>'','default'=>'',
'tooltip'=>'','errormsg'=>'','placeholder'=>'','convert'=>'','protect'=>'0','mask'=>'','data'=>'');}
?>
Ergebnis: Regeln, Labels und Statusoptionen kommen aus der FD; der Service dupliziert sie nicht.
4. Form-Template anlegen
Datei tpl/htm/contact-form.htm
<div id="dbxForm_1" class="dbx-panel dbxForm_wrapper dbx-ajax-root">
<div class="dbx-panel-head"><h2 class="h5">{form_title}</h2>
<a href="{list_url}" class="btn btn-outline-secondary btn-sm">Zur Liste</a></div>
<form action="{action}" method="post" id="dbx_form_1" class="dbxAjax" data-target="dbxForm_1">
<div class="dbx-panel-body">{form:message}<div class="row g-3">[dbx:form]</div></div>
<div class="dbx-panel-foot"><button type="submit" class="btn btn-primary">Speichern</button></div>
</form>
</div>
Prüfpunkt: Form-ID, Ajax-Ziel und DOM-ID gehören zusammen; {form:message} und [dbx:form] erscheinen jeweils genau einmal.
5. Service für Laden, Validieren und Speichern
Beginnen Sie include/myContactsService.class.php mit diesem vollständigen Formularpfad:
<?php
namespace dbx\myContacts;
class myContactsService
{
private const DD='myContacts|contact';
private const FORM_FD='myContacts|contact-form';
private const REPORT_FD='myContacts|contact-report';
private function url(string $run,array $params=array()):string{
$url='?dbx_modul=myContacts&dbx_run1='.rawurlencode($run);
foreach($params as $key=>$value)$url.='&'.rawurlencode((string)$key).'='.rawurlencode((string)$value);
return $url;
}
public function form():string{
$ridValue=(string)dbx()->get_modul_var('rid','new','parameter|max=24');
$rid=$ridValue==='new'?0:(int)$ridValue;$isNew=$rid<=0;
$form=dbx()->get_system_obj('dbxForm');
$form->init('contact-form',self::FORM_FD);
$form->set_data_source(self::DD,self::FORM_FD);$form->load_fd_messages();
$data=$isNew?array('status'=>'active'):dbx()->get_system_obj('dbxDB')->select1(self::DD,array('id'=>$rid));
if(!$isNew&&(int)($data['id']??0)<=0)return dbx()->get_system_obj('dbxTPL')->get_tpl(
'dbx|alert-warning',array('msg'=>$form->get_fd_message('not_found')));
$form->set_data(is_array($data)?$data:array())->set_rid($rid)
->set_action($this->url('form',array('rid'=>$isNew?'new':$rid)));
$form->_msg_info=$form->get_fd_message('form_info');
$form->add_rep('form_title',$form->get_fd_message($isNew?'form_title_new':'form_title_edit'));
$form->add_rep('list_url',$this->url('report'));$form->add_flds();
if($form->submit()){if(!$form->errors())$form->save_post(self::DD,$isNew?'new':$rid);
else $form->_msg_error=$form->get_fd_message('validation_error');}
return $form->run();
}
Jetzt testen: Öffnen Sie ?dbx_modul=myContacts&dbx_run1=form&rid=new, senden Sie zuerst einen leeren Namen und danach gültige Werte.
Erwartet: Der erste Submit bleibt mit FD-Validierung im Formular. Der zweite legt genau einen Datensatz an. Ein Reload ohne Submit erzeugt keinen zweiten Kontakt.
6. Filter-FD und dbxReport ergänzen
Datei fd/contact-report.fd.php
<?php
$messages=array('report_title'=>'Kontakte','filter_error'=>'Bitte Filter prüfen.',
'permission_denied_action'=>'Diese Aktion erfordert eine Administrator-Anmeldung.',
'invalid_contact'=>'Der angeforderte Kontakt ist ungültig.',
'delete_success'=>'Kontakt gelöscht.','delete_error'=>'Der Kontakt konnte nicht gelöscht werden.',
'delete_title'=>'Kontakt löschen','delete_question'=>'Kontakt 242 wirklich löschen?',
'delete_hint'=>'Diese Aktion kann nicht rückgängig gemacht werden.');
$fields=array(
array('name'=>'dbx_rwhere','type'=>'varchar','tpl'=>'dbx|search','default'=>'',
'label'=>'Suchen','rules'=>'sqlsearch|max=64'),
array('name'=>'dbx_rstatus','type'=>'varchar','tpl'=>'select-single-label','default'=>'all',
'label'=>'Status','rules'=>'parameter|max=16','options'=>'all=Alle&active=Aktiv&blocked=Gesperrt'));
?>
Ergänzen Sie im Service vor der schließenden Klassenklammer:
public function report(string $successKey='',string $errorKey=''):string{
$db=dbx()->get_system_obj('dbxDB');$report=dbx()->get_system_obj('dbxReport');
$report->init('contact-report',self::REPORT_FD);
$report->set_data_definition(self::DD)->set_mode('table')->set_action($this->url('report'))
->set_pagination(true,20)->set_table_actions(array());
$report->create_selection_fields(self::REPORT_FD);
$report->_msg_success=$successKey===''?'':$report->get_fd_message($successKey,$successKey);
$report->_msg_error=$errorKey===''?'':$report->get_fd_message($errorKey,$errorKey);
$search=trim((string)$report->get_fld_val('dbx_rwhere','','sqlsearch|max=64'));
$status=(string)$report->get_fld_val('dbx_rstatus','all','parameter|max=16');$where=array();
if(in_array($status,array('active','blocked'),true))$where['status']=$status;
if($search!=='')$where['search']=array('value'=>$search,'like'=>array('name','email','phone'),'mode'=>'contains');
$rows=$db->select(self::DD,$where,array('id','name','email','phone','status'),'name','ASC','',20,0);
$report->_rflds=array('name'=>'Name','email'=>'E-Mail','phone'=>'Telefon','status'=>'Status','action'=>'Aktion');
$report->_rpt_format=array('action'=>'html');
$report->_rrows=20;$report->_rpos=0;$report->_count_all=$db->count(self::DD);
$report->_rcount=$db->count(self::DD,$where);$report->_rdata=is_array($rows)?$rows:array();
return $report->run();
}
public function contact_report_next_record($report,$record){
if(!is_array($record))return $record;
$rid=(int)($record['id']??0);
$record['action']=dbx()->get_system_obj('dbxTPL')->get_tpl(
'myContacts|contact-row-action',array(
'edit_url'=>$this->url('form',array('rid'=>$rid)),
'delete_url'=>dbx()->action_url($this->url('delete',array('rid'=>$rid))),
'delete_title'=>$report->get_fd_message('delete_title'),
'delete_question'=>$report->format_fd_message('delete_question',array('id'=>$rid)),
'delete_hint'=>$report->get_fd_message('delete_hint')));
$status=(string)($record['status']??'');
if($status!=='')$record['status']=$report->get_fd_message('status_'.$status,$status);
return $record;
}
Datei tpl/htm/contact-report.htm
<div class="dbx-panel dbxReport dbx-ajax-root" id="dbx_target_1">
<div class="dbx-panel-head"><h2 class="h5">Kontakte</h2>
<a class="btn btn-primary" href="?dbx_modul=myContacts&dbx_run1=form&rid=new">Neu</a></div>
<form action="{action}" method="post" id="dbx_form_1" class="dbxAjax">
{report:bar}{report:message}[dbx:pagination]
<table class="table table-striped"><thead><tr>[rpt:row]</tr></thead>
<tbody><hr class="dbx_split"><tr>[rpt:row]</tr><hr class="dbx_split"></tbody></table>
</form>
</div>
Datei tpl/htm/contact-row-action.htm
<div class="btn-group btn-group-sm" role="group">
<a class="btn btn-outline-primary dbxAjax" href="{edit_url}"
aria-label="Kontakt bearbeiten">
<i class="bi bi-pencil" aria-hidden="true"></i>
</a>
<a class="btn btn-outline-danger dbxAjax dbxConfirm" href="{delete_url}"
data-confirm-title="{delete_title}" data-confirm="{delete_question}"
data-confirm-hint="{delete_hint}" data-confirm-buttons="yesno">
<i class="bi bi-trash" aria-hidden="true"></i>
<span class="visually-hidden">Löschen</span>
</a>
</div>
Jetzt testen: Legen Sie drei Kontakte an, filtern Sie nach einem Namensbestandteil und wechseln Sie den Statusfilter.
Erwartet: „Gesamt“ bleibt bei drei, die Trefferzahl ändert sich passend zum Filter. Ein ungültiger Status wird nicht in die DD-Abfrage übernommen.
7. Löschen serverseitig absichern
Der Link wird mit dbx()->action_url() erzeugt. Die zentrale Action-Policy prüft den Token; zusätzlich bleibt die Gruppe serverseitig verbindlich.
public function delete():string{
if(!dbx()->has_group('admin'))return $this->report('','permission_denied_action');
$rid=(int)dbx()->get_modul_var('rid',0,'int');
if($rid<=0)return $this->report('','invalid_contact');
$deleted=dbx()->get_system_obj('dbxDB')->delete(self::DD,array('id'=>$rid));
return $deleted===1?$this->report('delete_success'):$this->report('','delete_error');
}
}
Prüfpunkt: JavaScript darf „Wirklich löschen?“ fragen, aber weder Token noch Gruppe ersetzen. Testen Sie gültige, manipulierte und fehlende RID sowie eine nicht berechtigte Sitzung.
8. Normal- und Ajax-Request vergleichen
dbxAjax und stabile Ziel-IDs aktivieren die bestehende JavaScript-Pipeline. Es gibt keine zweite Ajax-Fachlogik.
Jetzt testen: Speichern und filtern Sie einmal mit aktiviertem und einmal mit deaktiviertem JavaScript.
Erwartet: Datensatz und Meldung sind identisch. Mit Ajax wird der Zielbereich ersetzt; ohne JavaScript lädt die vollständige Seite neu.
9. Vertragstest und Abnahme
Datei tests/myContactsContract_test.php
<?php
$root=dirname(__DIR__);$errors=array();
$required=array('myContacts.class.php','cfg/config.php','dd/contact.dd.php','fd/contact-form.fd.php',
'fd/contact-report.fd.php','include/myContactsService.class.php','tpl/htm/contact-form.htm','tpl/htm/contact-report.htm','tpl/htm/contact-row-action.htm');
foreach($required as $file)if(!is_file($root.'/'.$file))$errors[]='Fehlt: '.$file;
$php=file_get_contents($root.'/myContacts.class.php')."\n".file_get_contents($root.'/include/myContactsService.class.php');
foreach(array('/\bPDO\b/','/\bmysqli?_?/','/\bSQLite3\b/','/->(query|prepare|exec)\s*\(/')as $pattern)
if(preg_match($pattern,$php))$errors[]='Direkter Datenzugriff: '.$pattern;
foreach(array("get_system_obj('dbxDB')","get_system_obj('dbxForm')","get_system_obj('dbxReport')","has_group('admin')",'contact_report_next_record','action_url(','dbxConfirm','data-confirm-buttons="yesno"')as $needle)
if(strpos($php,$needle)===false)$errors[]='Vertrag fehlt: '.$needle;
if($errors){fwrite(STDERR,"FAIL\n- ".implode("\n- ",$errors)."\n");exit(1);}echo "OK myContacts architecture contract\n";
php -l dbx/modules/myContacts/myContacts.class.php
php -l dbx/modules/myContacts/include/myContactsService.class.php
php dbx/modules/myContacts/tests/myContactsContract_test.php
php dbx/modules/dbxSelfTest/tools/run.php --profile=full
Erwartet: Syntax, Modulvertrag und vollständiger SelfTest enden ohne Fehler. Prüfen Sie danach Tastatur, Mobilansicht und Rechte und schreiben Sie genau einen Change-Log-Eintrag für den abgeschlossenen logischen Block.