CODING.md 14.1 KB
Newer Older
Carsten  Rose's avatar
Carsten Rose committed
1
2
Design / Notes / Best Practices for Coding
==========================================
Carsten  Rose's avatar
Carsten Rose committed
3
4
5
6

General
=======

Carsten  Rose's avatar
Carsten Rose committed
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
* Class QuickFormQuery is the main entry point called by:
  * T3 Extension 'QFQ': called once per tt_content-record. 'bodytext' will be transferred to class QuickFormQuery.
    * The 'bodytext' contains:
      * Report definiton: 10.sql=SELECT ...
      * Form definition (explizit): form=Person
      * <Empty>: do nothing
  * api/save.php: wrapper to receive AJAX post request and instantiate QuickFormQuery.
  * api/load.php: not implemented yet. 
    * Wrapper to receive AJAX get requests.
    * delivers data for jqw grid
    * delivers data for typeahed fields
    * delivers data for select list
    * delivers data for depended (user select/unselect former elements) form elements

LOAD
----
* When qfq starts, 
  * (Form) Looking for a formname at: 
    1. Typo3 Bodytext Element,
Carsten  Rose's avatar
#2067    
Carsten Rose committed
26
    2. For the 'SIP' ($_GET['s'] => $S_SESSION['qfq'][$_GET['s']]="form=person&r=123")
Carsten  Rose's avatar
Carsten Rose committed
27
28
29
30
31
32
    3. $_GET variables 'form' and 'r' (=recordId) - the parameter 'form' has to be allowed in 'Permit URL Parameter' of 
       the specified form. This means: load the form to check, if it is allowed to load the form!?
    * If a formname is found, the search stops and the specified form will be processed.
  * (Report)
    * Process all <number>.[<number.>].sql statements

33
* Access variables:     
Carsten  Rose's avatar
Carsten Rose committed
34
35
36
  * active/valid formname: [$this->store->setVar(SYSTEM_FORM, $formName, STORE_SYSTEM);]
  * SIP: [$this->store->getVar('form', STORE_SIP)]
  * All parameters from active SIP:  [$this->store->getStore(STORE_SIP)]
37
  * Check Contstants.php for known Store *members*.
Carsten  Rose's avatar
Carsten Rose committed
38
  
Carsten  Rose's avatar
#2067    
Carsten Rose committed
39
* In QuickFormQuery.php the whole Form will be copied to `$this->formSpec` and depending on further processing, the 
40
41
42
43
44
45
46
47
48
49
  elements are available in `$this->feSpecNative` and `$this->feSpecAction`.
  * The Form specificaton (table form) will be evaluated direct after loading (no `dynamicUpdate`).
  * The FormElement specification will be evaluated later in BuildForm*.php:
    * $formSpec, and root elements of $feSpecAction & $feSpecNative will be read in QuickFormQuery.php.
    * AbstractBuildForm.php/BuildFormBootstrap.php receives a copy during instantiating of that class.
    * Processing of FormElements in  AbstractBuildForm.php/BuildFormBootstrap.php typically do not change values, 
      especially there is no evaluation, in $feSpecAction & $feSpecNative. 
  * *Dynamic Update*: remember, an update(=Form load) is a complete new rendering of the form in that moment. All 
    values/elements/notes/debug are fresh with the latest form content.  
  
Carsten  Rose's avatar
#2067    
Carsten Rose committed
50
51
* If a form is called without a SIP (form.permitNew='always'), than a SIP is created on the fly (as a 
  parameter in the form).
52
53
54
55
56
57
58
* Uniq SIP for mutliple tabs with r=0  
  * Depending on `r=0` or `r>0` a form submit will do an MySQL `insert` or `update` later during save.
  * For new records (r=0), clicking on 'save' without closing the form is a tricky situation. Additionally the user might have open multiple
      tabs (same form, all r=0) and after saving the record (wihtout closing the form) the user expects that it's ok to edit 
      the record again and again. Unfortunately, the initial created SIP (before 'form load') is not uniqe anymore (multiple 
      tabs might  contain a saved 'new record'). To guarantee correct saving of r=0 records, a unique on the fly generated SIP 
      is creatd during form load - individually per browser tab.  
59
60
* Faking the STORE_TYPO3 for API calls:
  * The PHP code api/save.php, api/load.php is called directly, without any TYPO3 Framework. Therefore the Typo3 information 
Carsten  Rose's avatar
Carsten Rose committed
61
    'pageId', 'feUser*', 'beUser*',  'ttContentUid', ... is not available.
62
63
64
65
66
  * *Form load*: an additional hidden Formelement '_sipForTypo3Vars' will be created with a subset of the current 
    STORE_TYPO3 values. The workaround with the SIP is usefull, cause the same form can be shown on different places (QFQ records) -
    this is not very likely, but might happen. The 'on the fly rendered' SIP helps to deliver the status.
    AbstractBuildForm.php: process() > prepareT3VarsForSave() > Store.php: copyT3VarsToSip();
  * *Form save*:  FillStoreForm.php: process() > Store: fillTypo3StoreFromSip()   
67

68
69
70
71
72
73
74
* Store: STORE_ADDITIONAL_FORM_ELEMENTS
  * HTML 'hidden' elements, inside of a checkbox or radio input definition, might disturb Bootstrap CSS classes. 
  * Therefore HTML elements like 'hidden' can be collected in STORE_ADDITIONAL_FORM_ELEMENTS.
  * When the form will be composed of all single parts, the STORE_ADDITIONAL_FORM_ELEMENTS content will be arranged before
    the regular INPUT Elements.
  
  
75
* Formular zusammenbauen
76
77
78
79
80
  * QuickFormQuery: doForm > loadFormSpecification  - laedt den Form Record alle Form Elemente die nicht genested sind: 
    native, pill, fieldset, templateGroup >> $his->formNative.
  
  * In `$this->feSpecNative` die Parameter Values
    
81
82
  * Damit wird '(BuildFormBootstrap / AbstractBuildForm) > process()' aufgerufen.
    * Hier wird AbstractBuildForm->elements() aufgerufen (ein Aufruf fuer alle root elemente).
83
    * Pro native/container Element (inkl. pill, fieldset, templateGroup) wird $builElementFunctionName aufgerufen. 
84
85
86
87
88
      - buildText()
      - ....
      - buildFieldSet()       << von hier werden alle zum aktuellen 'FieldSet' gehoerenden SubElemente abgearbeitet - via AbstractBuildForm->elements()  (damit schliesst sich der Kreis und wird rekursiv)
      - buildPill()           << von hier werden alle zum aktuellen 'Pill' gehoerenden SubElemente abgearbeitet - via AbstractBuildForm->elements()  (damit schliesst sich der Kreis und wird rekursiv)
      - buildTemplateGroup()  << von hier werden alle zum aktuellen 'Pill' gehoerenden SubElemente abgearbeitet - via AbstractBuildForm->elements()  (damit schliesst sich der Kreis und wird rekursiv)
89
90
      
    * Pro Container Element werden alle zugeordneten native/container elemente aufgerufen (beliebig tiefe Verschachtelung).
91
        
Carsten  Rose's avatar
Carsten Rose committed
92
93
94
95
SAVE
----
* Via wrapper api/save.php
* SID must be supplied via FORM POST
Carsten  Rose's avatar
Carsten Rose committed
96
* The SID supplies the <formname> and the <recordid>
Carsten  Rose's avatar
Carsten Rose committed
97
98
99
100
* form.render: plain/table/bootstrap
  * Client will handle the response of save.php.
  * Optional redirection initiated by client.
  
Carsten  Rose's avatar
#2067    
Carsten Rose committed
101
102
103
104
105
106
107
108
109
110
111
112
New records
...........
* r=0 (missing 'r' means r=0)
* After saving the SIP content will be updated with the new record. 
  Remember that the SIP in the URL is *not* the SIP used in the form to identify the form/record. The form use a 
  individual 'new record' SIP.

Existing records
................
* r>0 ('r' have to exist)

  
113
114
115
116
117
118
119
120
121
DELETE
------
* Via wrapper api/delete.php
* The element who should dissappear after successfull deleting: class=record
* Button:

  * class=record-delete
  * Button: data-sip={{SIP}}

122
123
124
125
126
127
128
129
   * SIP values:

     * SIP_RECORD_ID: Mandatory.
     * SIP_TABLE: Either SIP_TABLE or SIP_FORM has to be given.
     * SIP_FORM: Either SIP_TABLE or SIP_FORM has to be given. Not implemented now.
     * SIP_TARGET_URL: Only with SIP_MODE_ANSWER=MODE_HTML - Url to redirect browser to. 
     * SIP_MODE_ANSWER: MODE_JSON / MODE_HTML. If not given, this means MODE_JSON. 

130
131
* Three possible variants with delete links:
  
132
   * (1) Form: main record
133
134
135
136
   * HTML Code:
   
     <button id="delete-button" type="button" class="btn btn-default navbar-btn" ><span class="glyphicon glyphicon-trash"></span></button>
   
137
   * (2) Form: subrecord, one delete button per record 
138
139
140
   * HTML Code:
   
     <button type="button" class="record-delete" data-sip={{SIP}} ><span class="glyphicon glyphicon-trash"></span></button>
141
142
143
144
145
146
     
   * (3) Report: typially inside a table, but maybe different.

     <button type="button" class="record-delete" data-sip={{SIP}} ><span class="glyphicon glyphicon-trash"></span></button>


147
148
149
150
Upload
-----------------

* The upload UI consist of three elements
151
  * 1) A <div> tag with a) an optional filename of an earlier uploaded file and b) a trash button.
152
  * 2) The 'browse' button (<input type='file' name='<feName>'>). This element will not be send by post.
153
  * 3) A HTML hidden element with name=<feName> containing the <sipUpload>.
154
155
156
157
* A new uniq SIP (sipUpload) will be created for every upload formElement. These 'sipUpload' will be assigned to the upload 
  browse button and to the upload delete button.  
  * The individual sipUpload is necessary to correctly handle multiple simultaenously forms when using r=0. Also, through 
    this uniq id it's easy to distinguish between asynchron uploaded files.
158
159
  * The SIP on ther server contains the individual '_FILES' information submitted during the upload.
* Via the hidden element <feName> 'save()', access to the form individual upload status informations is given. 
160
161
162

Upload to server, before 'save'
...............................
163
164
* If a user open's a file for upload via the browse button, that file is immediately transmitted to the server. The user 
  will see a turning wheel until the upload finished.
165
* After successfull upload the 'Browse' button disappears and the filename, plus the delete button, will be displayed (client logic). 
166
* The uploaded file will be checked: maxsize, mime type, check script.
167
168
* The uploaded file is still temporary. It has been renamed from '[STORE_EXTRA][<uploadSip>][FILES_TMP_NAME]' to  
  '[STORE_EXTRA][<uploadSip>][FILES_TMP_NAME].cached'.
169
170
171
172
173
* The upload action will be saved in the user session:
    [STORE_EXTRA][<uploadSip>][FILES_TMP_NAME]
    [STORE_EXTRA][<uploadSip>][FILES_NAME]
    [STORE_EXTRA][<uploadSip>][FILES_ERROR]
    [STORE_EXTRA][<uploadSip>][FILE_SIZE]
174
* Clicks the user on delete button:
175
176
  * In the usersession a flagDelete will be set: 
     [STORE_EXTRA][<uploadSip>][FILES_FLAG_DELETE]='1'
177
  * An optional previous upload file (still not saved on the final place) will be deleted.
178
179
  * An optional existing variable [STORE_EXTRA][<uploadSip>][FILES_TMP_NAME] will be deleted. The 'flagDelete' must not 
    be change - it's later needed to detect to delete earlier uploaded files.
180
181
182

Form save
.........
183
* Step 1: insert / update the record. 
Carsten  Rose's avatar
Carsten Rose committed
184
* Step 2: process all 'uploads'.
185
186
187
188
189
190
    * Get every uniq uploadSip (=[STORE_CLIENT][<feName>]) of every upload formElement. Get the corresponding temporary uploaded filename.
    * If [STORE_EXTRA][<uploadSip>][FILES_FLAG_DELETE]='1' is set, delete previous uploaded file.
    * If [STORE_EXTRA][<uploadSip>][FILES_TMP_NAME]!='': indicates that there is an upload.
    * Calculate <fileDestination>
    * mv <file>.cached <fileDestination>
    * clientvalue[<feName>] = <fileDestination>
191
    * delete [STORE_EXTRA][<uploadSip>]
192
* Step 3: update record with final <fileDestination>    
193

194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
Formelement type: DATE / DATETIME / TIME
----------------------------------------
 * Available Formats:
   * 'yyyy-mm-dd' = FORMAT_DATE_INTERNATIONAL. 
   * 'dd.mm.yyyy' = FORMAT_DATE_GERMAN.
 * The 'DATE_FORMAT' can be specified systemwide in `config.ini`
 * The default format is FORMAT_DATE_INTERNATIONAL.
 * Optional: 'dateFormat' can be specified per form element in `form.parameter` - this overwrites 'systemwide'.
 * If there is no placeholder defined on the form element, the defined dateFormat is shown as placeholder.
 
 * Browser: 
   * checks the input with a system regexp.
   * regexp might be user defined. If given, do not use system regexp!
   * No min/max check.
 * Server: 
   * check with system wirde regexp 
   * regexp might be user defined. If given, do not use system regexp!
   * Do min/max check.
 
 * MySQL data: 1000-01-01 - 9999-12-31 and 0000-00-00
 * MySQL time: 00:00:00 - 23:59:59
 
 * datetime format: 'DATE TIME'

Carsten  Rose's avatar
Carsten Rose committed
218
219
Debug / Log
===========
Carsten  Rose's avatar
Carsten Rose committed
220
221
222

* Before firing a SQL or doing processing of an FormElement, set some debugging / error variables:

223
    [src] $this->store->setVar(SYSTEM_SQL_RAW, STORE_SYSTEM)
Carsten  Rose's avatar
Carsten Rose committed
224
225

* Available fields:
226
227
228
229
230
231
232
233
234
235

    <code>
    SYSTEM_SQL_RAW
    SYSTEM_SQL_FINAL
    SYSTEM_SQL_COUNT
    SYSTEM_SQL_PARAM_ARRAY
    SYSTEM_FORM = CLIENT_FORM;                        // '<formName> / <formId>'
    SYSTEM_FORM_ELEMENT = 'formElement';              // '<formElementName> / <formElementeId>'
    SYSTEM_FORM_ELEMENT_COLUMN = 'formElementColumn'; // '<columnname of current processed formElement>'
    </code>
Carsten  Rose's avatar
Carsten Rose committed
236

Carsten  Rose's avatar
Carsten Rose committed
237
238
* Form.debugShowInfo: yes|no will display a tooltip near beside every formelement and show parse/evaluate as tooltip.

239
240
241
242
* Check to display debug info:
 
    $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes'

Carsten  Rose's avatar
Carsten Rose committed
243
244
245
246
247
248
249
250
251
252
253
254
255
256
Errormessages & Eceptions
=========================

* Exception types:
  * Code 
  * Db
  * User Form
  * user Report 
  * plus an Errorhandler which throws exceptions
  
* Exceptions inside of an API call delivers the error code and msg as JSON to the client.
* Typo3 suppress E_NOTICE (e.g. undefined index). To catch E_NOTICE in QFQ, it will be temporaly enabled in QfqCongroller.php.
  

Carsten  Rose's avatar
Carsten Rose committed
257
258
259
Stores
======

260
* Retrieve 'get' or 'post' variables by:
Carsten  Rose's avatar
Carsten Rose committed
261
262
263
264
265
266
267
268
269
270
271
272
273
274

[src] $values = $this->store->getStore(STORE_CLIENT)



Primary Table
=============
* For the primary table all informations are available in STORE_TABLE_DEFAULT and STORE_TABLE_COLUMN_TYPES.
* Get all columns of the primary table by

[src] array_keys($this->getStore(STORE_TABLE_COLUMN_TYPES))

* Get the recent record in STORE_RECORD and the parent record (multiforms) in STORE_PARENT_RECORD.

Carsten  Rose's avatar
Carsten Rose committed
275
276
277

Typo3
=====
Carsten  Rose's avatar
Carsten Rose committed
278
279
280
* PageId: $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3)
  * Based on: $GLOBALS["TSFE"]->id   current Page
 
Carsten  Rose's avatar
Carsten Rose committed
281
282
283
284
285
286
287
288
* $GLOBALS["TSFE"]->fe_user->user["uid"]   fe_user_uid

* https://wiki.typo3.org/Extbase_HowTos
  * Old: $this->cObj->data['bodytext']
  * New:
        $contentObject = $this->configurationManager->getContentObject();
        $configuration = $contentObject->data['bodytext'];

289
290
291
* Verzeichnisstruktur Extension: https://docs.typo3.org/typo3cms/CoreApiReference/ExtensionArchitecture/FilesAndLocations/Index.html
* http://api.typo3.org/
  * http://api.typo3.org/typo3cms/62/html/index.html
Carsten  Rose's avatar
Carsten Rose committed
292
293
294
295
296
297
* https://docs.typo3.org/typo3cms/CoreApiReference/Introduction/Index.html

SIP
===
Page loaded: www.example.com?index.php&id=start&s=badcaffee1234&type=2&L=3, with $_SESSION['badcaffee1234'] => 'form=Person&r=1'

298
299
* $_SESSION['qfq'][$sip] => <urlparam>  >> $_SESSION['qfq']['badcaffee1234'] => 'form=Person&r=1'
* $_SESSION['qfq'][$urlparam] => <sip>  >> $_SESSION['qfq']['form=Person&r=1'] => 'badcaffee1234'
Carsten  Rose's avatar
Carsten Rose committed
300
301


302
303
304
305
306
307
308
309
310
311
FormElement
===========

Checkbox
--------

    <div class="checkbox">
        <label>
            <input type="checkbox">label 1
        </label>
Carsten  Rose's avatar
Carsten Rose committed
312
313
    </div>
    
314