-
Notifications
You must be signed in to change notification settings - Fork 1
/
Form.php
415 lines (345 loc) · 12.6 KB
/
Form.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
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
<?php
namespace PBF;
/*This project's namespace structure is leveraged to autoload requested classes at runtime.*/
function Load($class) {
$file = __DIR__ . "/../" . str_replace("\\", DIRECTORY_SEPARATOR, $class) . ".php";
if(is_file($file))
include_once $file;
}
spl_autoload_register("PBF\Load");
if(in_array("__autoload", spl_autoload_functions()))
spl_autoload_register("__autoload");
class Form extends Base {
protected $_elements = array();
protected $_prefix = "http";
protected $_values = array();
protected $_attributes = array();
protected $ajax;
protected $ajaxCallback;
protected $errorView;
protected $labelToPlaceholder;
protected $resourcesPath;
/*Prevents various automated from being automatically applied. Current options for this array
included jQuery, bootstrap and focus.*/
protected $prevent = array();
protected $view;
public function __construct($id = "pbf") {
$this->configure(array(
"action" => basename($_SERVER["SCRIPT_NAME"]),
"id" => preg_replace("/\W/", "-", $id),
"method" => "post"
));
if(isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on")
$this->_prefix = "https";
/*The Standard view class is applied by default and will be used unless a different view is
specified in the form's configure method*/
if(empty($this->view))
$this->view = new View\SideBySide;
if(empty($this->errorView))
$this->errorView = new ErrorView\Standard;
/*The resourcesPath property is used to identify where third-party resources needed by the
project are located. This property will automatically be set properly if the PBF directory
is uploaded within the server's document root. If symbolic links are used to reference the PBF
directory, you may need to set this property in the form's configure method or directly in this
constructor.*/
$path = __DIR__ . "/Resources";
if(strpos($path, $_SERVER["DOCUMENT_ROOT"]) !== false)
$this->resourcesPath = substr($path, strlen($_SERVER["DOCUMENT_ROOT"]));
else
$this->resourcesPath = "/PBF/Resources";
}
/*When a form is serialized and stored in the session, this function prevents any non-essential
information from being included.*/
public function __sleep() {
return array("_attributes", "_elements", "errorView");
}
public function addElement(Element $element) {
$element->_setForm($this);
//If the element doesn't have a specified id, a generic identifier is applied.
$id = $element->getAttribute("id");
if(empty($id))
$element->setAttribute("id", $this->_attributes["id"] . "-element-" . sizeof($this->_elements));
$this->_elements[] = $element;
/*For ease-of-use, the form tag's encytype attribute is automatically set if the File element
class is added.*/
if($element instanceof Element\File)
$this->_attributes["enctype"] = "multipart/form-data";
}
/*Values that have been set through the setValues method, either manually by the developer
or after validation errors, are applied to elements within this method.*/
protected function applyValues() {
foreach($this->_elements as $element) {
$name = $element->getAttribute("name");
if(isset($this->_values[$name]))
$element->setAttribute("value", $this->_values[$name]);
elseif(substr($name, -2) == "[]" && isset($this->_values[substr($name, 0, -2)]))
$element->setAttribute("value", $this->_values[substr($name, 0, -2)]);
}
}
public static function clearErrors($id = "pbf") {
if(!empty($_SESSION["pbf"][$id]["errors"]))
unset($_SESSION["pbf"][$id]["errors"]);
}
public static function clearValues($id = "pbf") {
if(!empty($_SESSION["pbf"][$id]["values"]))
unset($_SESSION["pbf"][$id]["values"]);
}
public function getAjax() {
return $this->ajax;
}
public function getElements() {
return $this->_elements;
}
public function getErrorView() {
return $this->errorView;
}
public function getPrefix() {
return $this->_prefix;
}
public function getPrevent() {
return $this->prevent;
}
public function getResourcesPath() {
return $this->resourcesPath;
}
public function getErrors() {
$errors = array();
if(session_id() == "")
$errors[""] = array("Error: The pbf project requires an active session to function properly. Simply add session_start() to your script before any output has been sent to the browser.");
else {
$errors = array();
$id = $this->_attributes["id"];
if(!empty($_SESSION["pbf"][$id]["errors"]))
$errors = $_SESSION["pbf"][$id]["errors"];
}
return $errors;
}
protected static function getSessionValues($id = "pbf") {
$values = array();
if(!empty($_SESSION["pbf"][$id]["values"]))
$values = $_SESSION["pbf"][$id]["values"];
return $values;
}
public static function isValid($id = "pbf", $clearValues = true) {
$valid = true;
/*The form's instance is recovered (unserialized) from the session.*/
$form = self::recover($id);
if(!empty($form)) {
if($_SERVER["REQUEST_METHOD"] == "POST")
$data = $_POST;
else
$data = $_GET;
/*Any values/errors stored in the session for this form are cleared.*/
self::clearValues($id);
self::clearErrors($id);
/*Each element's value is saved in the session and checked against any validation rules applied
to the element.*/
if(!empty($form->_elements)) {
foreach($form->_elements as $element) {
$name = $element->getAttribute("name");
if(substr($name, -2) == "[]")
$name = substr($name, 0, -2);
/*The File element must be handled differently b/c it uses the $_FILES superglobal and
not $_GET or $_POST.*/
if($element instanceof Element\File)
$data[$name] = $_FILES[$name]["name"];
if(isset($data[$name])) {
$value = $data[$name];
if(is_array($value)) {
$valueSize = sizeof($value);
for($v = 0; $v < $valueSize; ++$v)
$value[$v] = stripslashes($value[$v]);
}
else
$value = stripslashes($value);
self::_setSessionValue($id, $name, $value);
}
else
$value = null;
/*If a validation error is found, the error message is saved in the session along with
the element's name.*/
if(!$element->isValid($value)) {
self::setError($id, $element->getErrors(), $name);
$valid = false;
}
}
}
/*If no validation errors were found, the form's session values are cleared.*/
if($valid) {
if($clearValues)
self::clearValues($id);
self::clearErrors($id);
}
}
else
$valid = false;
return $valid;
}
/*This method restores the serialized form instance.*/
protected static function recover($id) {
if(!empty($_SESSION["pbf"][$id]["form"]))
return unserialize($_SESSION["pbf"][$id]["form"]);
else
return "";
}
public function render($returnHTML = false) {
if(!empty($this->labelToPlaceholder)) {
foreach($this->_elements as $element) {
$label = $element->getLabel();
if(!empty($label)) {
$element->setAttribute("placeholder", $label);
$element->setLabel("");
}
}
}
$this->view->_setForm($this);
$this->errorView->_setForm($this);
/*When validation errors occur, the form's submitted values are saved in a session
array, which allows them to be pre-populated when the user is redirected to the form.*/
$values = self::getSessionValues($this->_attributes["id"]);
if(!empty($values))
$this->setValues($values);
$this->applyValues();
if($returnHTML)
ob_start();
$this->renderCSS();
$this->view->render();
$this->renderJS();
/*The form's instance is serialized and saved in a session variable for use during validation.*/
$this->save();
if($returnHTML) {
$html = ob_get_contents();
ob_end_clean();
return $html;
}
}
/*When ajax is used to submit the form's data, validation errors need to be manually sent back to the
form using json.*/
public static function renderAjaxErrorResponse($id = "pbf") {
$form = self::recover($id);
if(!empty($form))
$form->errorView->renderAjaxErrorResponse();
}
protected function renderCSS() {
$this->renderCSSFiles();
echo '<style type="text/css">';
$this->view->renderCSS();
$this->errorView->renderCSS();
foreach($this->_elements as $element)
$element->renderCSS();
echo '</style>';
}
protected function renderCSSFiles() {
$urls = array();
if(!in_array("bootstrap", $this->prevent))
$urls[] = $this->_prefix . "://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css";
foreach($this->_elements as $element) {
$elementUrls = $element->getCSSFiles();
if(is_array($elementUrls))
$urls = array_merge($urls, $elementUrls);
}
/*This section prevents duplicate css files from being loaded.*/
if(!empty($urls)) {
$urls = array_values(array_unique($urls));
foreach($urls as $url)
echo '<link type="text/css" rel="stylesheet" href="', $url, '"/>';
}
}
protected function renderJS() {
$this->renderJSFiles();
echo '<script type="text/javascript">';
$this->view->renderJS();
foreach($this->_elements as $element)
$element->renderJS();
$id = $this->_attributes["id"];
echo 'jQuery(document).ready(function() {';
/*When the form is submitted, disable all submit buttons to prevent duplicate submissions.*/
echo <<<JS
jQuery("#$id").bind("submit", function() {
jQuery(this).find("input[type=submit]").attr("disabled", "disabled");
});
JS;
/*jQuery is used to set the focus of the form's initial element.*/
if(!in_array("focus", $this->prevent))
echo 'jQuery("#', $id, ' :input:visible:enabled:first").focus();';
$this->view->jQueryDocumentReady();
foreach($this->_elements as $element)
$element->jQueryDocumentReady();
/*For ajax, an anonymous onsubmit javascript function is bound to the form using jQuery. jQuery's
serialize function is used to grab each element's name/value pair.*/
if(!empty($this->ajax)) {
echo <<<JS
jQuery("#$id").bind("submit", function() {
JS;
/*Clear any existing validation errors.*/
$this->errorView->clear();
echo <<<JS
jQuery.ajax({
url: "{$this->_attributes["action"]}",
type: "{$this->_attributes["method"]}",
data: jQuery("#$id").serialize(),
success: function(response) {
if(response != undefined && typeof response == "object" && response.errors) {
JS;
$this->errorView->applyAjaxErrorResponse();
echo <<<JS
jQuery("html, body").animate({ scrollTop: jQuery("#$id").offset().top }, 500 );
}
else {
JS;
/*A callback function can be specified to handle any post submission events.*/
if(!empty($this->ajaxCallback))
echo $this->ajaxCallback, "(response);";
/*After the form has finished submitting, re-enable all submit buttons to allow additional submissions.*/
echo <<<JS
}
jQuery("#$id").find("input[type=submit]").removeAttr("disabled");
}
});
return false;
});
JS;
}
echo '}); </script>';
}
protected function renderJSFiles() {
$urls = array();
if(!in_array("jQuery", $this->prevent))
$urls[] = $this->_prefix . "://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js";
if(!in_array("bootstrap", $this->prevent))
$urls[] = $this->_prefix . "://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js";
foreach($this->_elements as $element) {
$elementUrls = $element->getJSFiles();
if(is_array($elementUrls))
$urls = array_merge($urls, $elementUrls);
}
/*This section prevents duplicate js files from being loaded.*/
if(!empty($urls)) {
$urls = array_values(array_unique($urls));
foreach($urls as $url)
echo '<script type="text/javascript" src="', $url, '"></script>';
}
}
/*The save method serialized the form's instance and saves it in the session.*/
protected function save() {
$_SESSION["pbf"][$this->_attributes["id"]]["form"] = serialize($this);
}
/*Valldation errors are saved in the session after the form submission, and will be displayed to the user
when redirected back to the form.*/
public static function setError($id, $errors, $element = "") {
if(!is_array($errors))
$errors = array($errors);
if(empty($_SESSION["pbf"][$id]["errors"][$element]))
$_SESSION["pbf"][$id]["errors"][$element] = array();
foreach($errors as $error)
$_SESSION["pbf"][$id]["errors"][$element][] = $error;
}
protected static function _setSessionValue($id, $element, $value) {
$_SESSION["pbf"][$id]["values"][$element] = $value;
}
/*An associative array is used to pre-populate form elements. The keys of this array correspond with
the element names.*/
public function setValues(array $values) {
$this->_values = array_merge($this->_values, $values);
}
}