-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathOnceForm.php
396 lines (333 loc) · 11.2 KB
/
OnceForm.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
<?php
require_once 'validators.php';
require_once 'OnceFields.php';
/*
The OnceForm - Write once HTML5 forms processing for PHP.
Copyright (C) 2012-2013 adcSTUDIO LLC
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 2 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, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
/**
* The OnceForm Write your form once, wrap it in a function.
* Pass that function name to a new OnceForm and it wires up the
* request validation!
*
* @author Kevin Newman <[email protected]>
* @package The OnceForm
* @copyright (C) 2012 adcSTUDIO LLC
* @license GNU/GPL, see license.txt
*/
class OnceForm
{
public $isValid = false;
public $validators = array();
public $data = array();
public $form_html;
public $form_func;
public $form;
public $doc;
public $xpath;
public $fields = array();
protected $user_validator;
public function __toString()
{
// different versions of php handle this differently
// see: http://us2.php.net/manual/en/domdocument.savehtml.php
if ( version_compare(PHP_VERSION, '5.3.6', '>=')) {
// for current versions, we need to pass a node, to get the fragment HTML
return $this->doc->saveHTML( $this->form );
}
elseif ( version_compare(PHP_VERSION, '5.2.6', '>=')) {
// for php 5.2.6 through 5.3.5 we need to trim this manually
return preg_replace('/^<!DOCTYPE.+?>/', '',
str_replace( array('<html>', '</html>', '<body>', '</body>'), array('', '', '', ''),
preg_replace('/<head\b[^>]*>(.*?)<\/head>/','',$this->doc->saveHTML())
)
);
}
else
// older than 5.2.6 only saves the fragment you pass it
return $this->doc->saveHTML();
}
public function toString() {
return $this->__toString();
}
/**
* Creates a OnceForm.
*
* @param function $form_func A function set up to output (echo) an
* HTML5 to the user. This function's output will be captured to an
* output buffer. Note: If a form func is passed to the constructor,
* the OnceForm will automatically check and validate the request.
*/
public function __construct( $form_func = NULL, $validator = NULL )
{
if ( is_callable( $form_func ) )
$this->form_func = $form_func;
else
$this->form_html = $form_func;
$this->user_validator = $validator;
if ( !is_null( $form_func ) )
$this->init();
}
/**
* Sets up the OnceForm. a form func or form html should be set
* before calling init (usually through the constructor).
* @return void
*/
public function init()
{
$this->init_form();
$this->init_request();
}
protected function init_form()
{
if ( is_callable( $this->form_func ) )
$this->capture_form( $this->form_func );
$this->parse_form();
$this->extract_fields();
}
protected function init_request()
{
// get the request data
if ( $data = $this->get_request_data() ) {
// verify, and set this new data
$this->set_data( $data );
$this->isValid = $this->validate();
}
else {
// use the default data if not a request
$this->data = $this->get_default_data();
}
}
/**
* Runs the form func, and captures the resulting html.
*
* @param function $func A function setup to output (echo) an
* HTML5 to the user. This function's output will be captured to an
* output buffer.
*/
public function capture_form( $func )
{
$this->form_func = $func;
ob_start();
call_user_func( $func );
$this->form_html = ob_get_clean();
}
/**
* Set the form to process from an HTML string.
*
* @param string $html The html5 form to be automatically processed.
*/
public function parse_form( $html = NULL )
{
if ( !is_null( $html ) )
$this->form_html = $html;
$html = $this->form_html;
// extract encoding from the html string
$encoding = mb_detect_encoding( $html );
$this->doc = new DOMDocument( '', $encoding );
// DOMDocument needs a complete document, along with a charset encoding.
$this->doc->loadHTML( '<html><head>
<meta http-equiv="content-type" content="text/html; charset='.
$encoding.'"></head><body>' . trim( $html ) . '</body></html>' );
// grab a reference to the form element
$body = $this->doc->getElementsByTagName( 'body' );
$this->form = $body->item( 0 )->firstChild;
}
/**
* Extracts the fields from the form based on the registered
* field types.
*/
public function extract_fields()
{
// setup xpath for individual element extraction.
$xpath = $this->xpath = new DOMXpath($this->doc);
$this->fields = array();
// loop and extract each
foreach ( self::$fieldTypes as $field_type ) {
$this->fields = array_merge(
$this->fields, $field_type->extract( $xpath )
);
}
}
/**
* Checks the PHP GP objects, to see if a request has been made.
* Called automatically in init.
*/
public function get_request_data()
{
$form = $this->form;
if ( 'POST' == strtoupper( $form->getAttribute('method') ) )
$data = $_POST;
else
// the default `method` if none specified is GET
$data = $_GET;
return $data;
}
public function is_request()
{
return $this->get_request_data();
}
public function is_valid()
{
return $this->isValid;
}
/**
* Sets all fields with values in $data. Missing fields are set empty.
* In this way, the data is "polyfilled" so you don't have to worry
* about doing isset on every key. This method will set the field
* values of all fields from request data, but only the keys and
* values of enumerable fields are retained in the data property.
* When loading and setting stored form data, you may wish to avoid
* setting the hidden (unemumerable) form fields.
* @param array $data An array of complete request data (even
* non-enumerable fields).
* @param bool $include_hidden Whether or not to also set enumerable
* fields. default: true
* @return array An associateive array of request data mixed
* with field data (enumerable fields only).
*/
public function set_data( array $data, $include_hidden = true )
{
// First get the field names (filtering enumerable fields).
$field_names = $this->get_field_names( $include_hidden );
$this->data = array();
foreach( $field_names as $name ) {
// get the field for this data
$field = $this->fields[ $name ];
// make sure any keys not present in $data are set to empty.
$value = ( isset( $data[ $name ] ) ) ? $data[ $name ]: '';
// :HACK: deal with the specific case of multiple select box
if ('select' == $field->field_type()->tag_name &&
strstr( $name, '[]') && $field->multiple() ) {
$altn = substr( $name, 0, -2 );
$value = ( isset( $data[ $altn ] ) ) ? $data[ $altn ]: '';
}
// set the field value, even when not enumerable (hidden)
$field->value( $value );
// if the field is not enumerable (hidden) don't set in $data
if ( !$field->field_type()->enumerable ) continue;
$this->data[ $name ] = $value;
}
return $this->data;
}
/**
* Starts with the form's default values, and mixes in the $data.
* In this way, the data is "polyfilled" so you don't have to worry
* about doing isset on every key.
* :NOTE: This does not set the data property, only returns
* @param array $data An array of complete request data (even
* non-enumerable fields).
* @return array An associateive array of request data mixed
* mixed with field data (enumerable fields only).
*/
public function mix_data_with_default( array $data, $include_hidden = false )
{
// First get the default data.
$default_data = $this->get_default_data( $include_hidden );
// Filters out any fields that aren't in the onceform.
$filtered_data = array();
foreach( $data as $key => $value ) {
if ( array_key_exists( $key, $default_data ) )
$filtered_data[ $key ] = $data[ $key ];
}
// Mix the filtered request data with the default data.
return array_merge( $default_data, $filtered_data );
}
/**
* Gets the default values (specified in the HTML) of the OnceForm.
* @return array The default data.
*/
public function get_default_data( $include_hidden = false )
{
$data = array();
foreach( $this->fields as $field ) {
if ( $field->field_type()->enumerable || $include_hidden )
$data[ $field->name() ] = $field->default_value();
}
return $data;
}
/**
* Gets the names of the enumerable fields in an array.
* NOTE: Names are in the original array syntax, not nested.
* (So, "name[one][two]" etc.)
* @return array The names of the fields.
*/
public function get_field_names( $include_hidden = false )
{
$names = array();
foreach( $this->fields as $field ) {
if ( $field->field_type()->enumerable || $include_hidden )
$names[] = $field->name();
}
return $names;
}
public function set_required( $name, $required = true ) {
$this->fields[ $name ]->required( $required );
}
/**
* Validates the OnceForm against the data passed. This method will
* create the validator objects and store them in $this->validators
* by name. To override a validator for a specific input element
* add it to $this->validators before calling validate().
*
* @param array $data The request data (or other) to validate
* against the OnceForm.
* @return boolean Whether or not the data is valid by the
* rules of the OnceForm.
*/
public function validate( $data = NULL )
{
if ( is_null( $data ) )
$data = $this->data;
$valid = true;
if ( $this->user_validator ) {
$errors = call_user_func( $this->user_validator, $data, $this );
$validator = new UserValidator( $errors );
if ( !$validator->isValid() )
$valid = false;
$this->validators[] = $validator;
}
foreach( $this->fields as $field ) {
$this->validators[] = $field->validator();
if ( ! $field->validity() )
$valid = false;
}
return $valid;
}
public function get_validation_errors()
{
$errors = array();
foreach( $this->validators as $validator )
$errors += $validator->errors();
return $errors;
}
public function set_user_validator( /* callable */ $func )
{
$this->user_validator = $func;
}
static protected $fieldTypes = array();
static public function addFieldType( FieldType $field )
{
self::$fieldTypes[] = $field;
}
}
OnceForm::addFieldType( new SubFieldType('input', 'text', 'InputField') );
OnceForm::addFieldType( new SubFieldType('input', 'submit', 'InputField') );
OnceForm::addFieldType( new SubFieldType('input', 'password', 'InputField') );
OnceForm::addFieldType( new SubFieldType('input', 'email', 'InputField', 'EmailValidator') );
//OnceForm::addFieldType( new FieldType('input', 'number', 'NumberField') );
OnceForm::addFieldType( new FieldType('select', 'SelectField') );
OnceForm::addFieldType( new FieldType('textarea', 'TextareaField') );
OnceForm::addFieldType( new SubFieldType('input', 'checkbox', 'CheckboxField') );
OnceForm::addFieldType( new RadioSetFieldType() );