| 1 |
<?php
|
| 2 |
/**
|
| 3 |
* BUGGY CONCEPT CODE!
|
| 4 |
*
|
| 5 |
* This is code for a object oriented Forms and Rendering API in Drupal 7.
|
| 6 |
*/
|
| 7 |
|
| 8 |
/**
|
| 9 |
* DrupalWidget extends DrupalElement and adds rendering functionality.
|
| 10 |
*
|
| 11 |
* DrupalWidget is to be used for non-input widgets like markup or table.
|
| 12 |
*
|
| 13 |
* Biggest TODO: Integrate with Drupal's theme system. Probably not easy,
|
| 14 |
* but certainly doable. In case of doubt we could just only use regular
|
| 15 |
* theme functions instead of a theme() method.
|
| 16 |
*/
|
| 17 |
abstract class DrupalWidget extends DrupalElement {
|
| 18 |
/**
|
| 19 |
* Constructor
|
| 20 |
*/
|
| 21 |
public function __construct($properties = array()) {
|
| 22 |
parent::__construct($properties);
|
| 23 |
// We're obviously not yet rendered.
|
| 24 |
$this['rendered'] = FALSE;
|
| 25 |
$this['render'] = TRUE;
|
| 26 |
}
|
| 27 |
|
| 28 |
/**
|
| 29 |
* Render the current element and all children.
|
| 30 |
* This works similar to drupal_render()...
|
| 31 |
*/
|
| 32 |
public function render($force = FALSE) {
|
| 33 |
if (!$force && !$this['render']) {
|
| 34 |
return;
|
| 35 |
}
|
| 36 |
if (!$force && $this['rendered']) {
|
| 37 |
return $this['renderedContent'];
|
| 38 |
}
|
| 39 |
|
| 40 |
$this->invoke('preRender');
|
| 41 |
|
| 42 |
if (isset($this['theme']) && is_callable($this['theme'])) {
|
| 43 |
$content = call_user_func_array($this['theme'], array(&$this));
|
| 44 |
}
|
| 45 |
else {
|
| 46 |
$content = $this->theme();
|
| 47 |
}
|
| 48 |
|
| 49 |
$this['rendered'] = TRUE;
|
| 50 |
$this['renderedContent'] = $content;
|
| 51 |
|
| 52 |
$this->invoke('postRender');
|
| 53 |
|
| 54 |
return $this['renderedContent'];
|
| 55 |
}
|
| 56 |
|
| 57 |
/**
|
| 58 |
* Render all children of the current element.
|
| 59 |
* Optionally wrap each children between $prefix and $suffix.
|
| 60 |
*/
|
| 61 |
public function renderChildren($prefix = '', $suffix = '') {
|
| 62 |
$output = "";
|
| 63 |
foreach (q($this)->children()->filterByType('DrupalWidget') as $child) {
|
| 64 |
$output .= $prefix . $child->render(FALSE) . $suffix;
|
| 65 |
}
|
| 66 |
return $output;
|
| 67 |
}
|
| 68 |
|
| 69 |
protected function theme() {
|
| 70 |
$output = "<div><span>". $this->toString() ."</span><br />";
|
| 71 |
$output .= $this->renderChildren();
|
| 72 |
$output .= '</div>';
|
| 73 |
return $output;
|
| 74 |
}
|
| 75 |
}
|
| 76 |
|
| 77 |
|
| 78 |
/**
|
| 79 |
* DrupalFormWidget extends DrupalWidget and adds functionality needed for form elements.
|
| 80 |
*
|
| 81 |
*/
|
| 82 |
abstract class DrupalFormWidget extends DrupalWidget {
|
| 83 |
public function __construct($properties = array()) {
|
| 84 |
$defaults = array(
|
| 85 |
'attributes' => array(),
|
| 86 |
'has_error' => FALSE,
|
| 87 |
'value' => '',
|
| 88 |
'default_value' => '',
|
| 89 |
);
|
| 90 |
parent::__construct(array_merge($defaults, $properties));
|
| 91 |
}
|
| 92 |
|
| 93 |
public function assignValue() {
|
| 94 |
if ($this['parent_form']['posted'] && $this['post_value']) {
|
| 95 |
$this['value'] = $this['post_value'];
|
| 96 |
}
|
| 97 |
else {
|
| 98 |
$this['value'] = $this['default_value'];
|
| 99 |
}
|
| 100 |
}
|
| 101 |
|
| 102 |
public function setError($message) {
|
| 103 |
$this['error_message'] = $message;
|
| 104 |
$this['has_error'] = TRUE;
|
| 105 |
$this['parent_form']['has_error'] = TRUE;
|
| 106 |
}
|
| 107 |
}
|
| 108 |
|
| 109 |
/**
|
| 110 |
* A form. This is where FAPI will live.
|
| 111 |
*
|
| 112 |
* Each individual form will be a class of its own that extends DrupalForm.
|
| 113 |
*/
|
| 114 |
abstract class DrupalForm extends DrupalWidget {
|
| 115 |
public function __construct($properties = array()) {
|
| 116 |
$defaults = array(
|
| 117 |
'form_id' => get_class($this),
|
| 118 |
'method' => 'POST',
|
| 119 |
'has_error' => FALSE,
|
| 120 |
'cache' => TRUE,
|
| 121 |
'posted' => FALSE,
|
| 122 |
'step' => 0,
|
| 123 |
);
|
| 124 |
// Call the parent constructor. This will also invoke 'construct'.
|
| 125 |
parent::__construct(array_merge($defaults, $properties));
|
| 126 |
|
| 127 |
// Add a hidden field with the form_id
|
| 128 |
$this->addChild('form_id', new DrupalHiddenElement(array('default_value' => $this['form_id'])));
|
| 129 |
// Add a submit button. TODO: only do this if there is no submit button so far.
|
| 130 |
$this->addChild('submit', new DrupalSubmitButton());
|
| 131 |
}
|
| 132 |
|
| 133 |
/**
|
| 134 |
* This is called whenever a new descendant element is added to the form (during construction)
|
| 135 |
*/
|
| 136 |
protected function descendantAdded($element) {
|
| 137 |
if ($element instanceof DrupalFormWidget) {
|
| 138 |
// We add $this as parent_form to the element
|
| 139 |
$element['parent_form'] = $this;
|
| 140 |
// And set the post_name to the path from $this to the element
|
| 141 |
$element['post_name'] = $element->createPath($this, $element);
|
| 142 |
}
|
| 143 |
}
|
| 144 |
|
| 145 |
public function build() {
|
| 146 |
$this['action'] = $_GET['q'];
|
| 147 |
|
| 148 |
// Check whether the form got posted
|
| 149 |
if (isset($_POST[$this['name'] .'/form_id']) && $_POST[$this['name'] .'/form_id'] == $this['form_id']) {
|
| 150 |
// Assign the POST values to each element. That's the last time that we deal with POST!
|
| 151 |
foreach ($_POST as $path => $value) {
|
| 152 |
$e = $this->findByPath($path);
|
| 153 |
$e['post_value'] = $value;
|
| 154 |
}
|
| 155 |
$this['posted'] = TRUE;
|
| 156 |
}
|
| 157 |
|
| 158 |
q($this)->descendants()->filterByType('DrupalFormWidget')->assignValue();
|
| 159 |
|
| 160 |
|
| 161 |
if ($this['posted']) {
|
| 162 |
$this->processPostedForm();
|
| 163 |
}
|
| 164 |
|
| 165 |
$this->cache();
|
| 166 |
}
|
| 167 |
|
| 168 |
/**
|
| 169 |
* This is called after the form is built, but only if the form got posted.
|
| 170 |
*/
|
| 171 |
private function processPostedForm() {
|
| 172 |
drupal_set_message("The form got posted!<br>POST:<pre>" . print_r($_POST, true) ."</pre>");
|
| 173 |
|
| 174 |
$this['has_error'] = FALSE;
|
| 175 |
|
| 176 |
// Validate.
|
| 177 |
|
| 178 |
q($this)->descendants()->filterByType('DrupalFormWidget')->set('has_error', FALSE)->invoke('validate');
|
| 179 |
|
| 180 |
// The form itself can also have a validate function.
|
| 181 |
$this->invoke('validate');
|
| 182 |
|
| 183 |
// If no validation errors, submit.
|
| 184 |
if (!$this['has_error']) {
|
| 185 |
drupal_set_message("No validation error ('error' in a textfield triggers an error). Let's invoke 'submit'.\n");
|
| 186 |
$this['step'] = $this['step'] + 1;
|
| 187 |
$this->invoke('submit');
|
| 188 |
}
|
| 189 |
// Otherwise, don't submit.
|
| 190 |
else {
|
| 191 |
drupal_set_message("Error. See below.\n");
|
| 192 |
}
|
| 193 |
}
|
| 194 |
|
| 195 |
public function cache() {
|
| 196 |
if (!$this['cache']) {
|
| 197 |
return;
|
| 198 |
}
|
| 199 |
// Add a build_id ...
|
| 200 |
if (!isset($this['build_id'])) {
|
| 201 |
$this['build_id'] = md5(mt_rand());
|
| 202 |
$this->addChild('form_build_id', new DrupalHiddenElement(array('default_value' => $this['build_id'])));
|
| 203 |
}
|
| 204 |
cache_set('form_build:'. $this['build_id'], $this);
|
| 205 |
}
|
| 206 |
|
| 207 |
public function redirect($target = NULL) {
|
| 208 |
if (isset($target)) {
|
| 209 |
drupal_goto($target);
|
| 210 |
}
|
| 211 |
elseif (isset($this['redirect'])) {
|
| 212 |
drupal_goto($this['redirect']);
|
| 213 |
}
|
| 214 |
else {
|
| 215 |
drupal_goto($_GET['q']);
|
| 216 |
}
|
| 217 |
}
|
| 218 |
|
| 219 |
public function theme() {
|
| 220 |
$output = '<form action="'. $this['action'] .'" method="'. $this['method'] . '">';
|
| 221 |
$output .= $this->renderChildren();
|
| 222 |
$output .= '</form>';
|
| 223 |
return $output;
|
| 224 |
}
|
| 225 |
}
|
| 226 |
|
| 227 |
|
| 228 |
|
| 229 |
/* Some basic widgets */
|
| 230 |
|
| 231 |
|
| 232 |
|
| 233 |
class DrupalHiddenElement extends DrupalFormWidget {
|
| 234 |
public function theme() {
|
| 235 |
return '<input type="hidden" name="'. $this['post_name'] .'" " value="'. $this['value'] ."\" ". drupal_attributes($this['attributes']) ." />\n";
|
| 236 |
}
|
| 237 |
}
|
| 238 |
|
| 239 |
class DrupalSubmitButton extends DrupalFormWidget {
|
| 240 |
public function __construct($properties = array()) {
|
| 241 |
$defaults = array('default_value' => 'submit');
|
| 242 |
parent::__construct(array_merge($defaults, $properties));
|
| 243 |
}
|
| 244 |
public function theme() {
|
| 245 |
return '<input type="submit" name="'. $this['post_name'] .'" " value="'. $this['value'] ."\" ". drupal_attributes($this['attributes']) ." />\n";
|
| 246 |
}
|
| 247 |
}
|
| 248 |
|
| 249 |
class DrupalTextfield extends DrupalFormWidget {
|
| 250 |
public function validate() {
|
| 251 |
if ($this['post_value'] == 'error') {
|
| 252 |
$this->setError('A validation error!');
|
| 253 |
}
|
| 254 |
}
|
| 255 |
public function theme() {
|
| 256 |
$output = '<div class="form=item">';
|
| 257 |
if ($this['has_error']) {
|
| 258 |
$output .= "<span class='error-message'>{$this['error_message']}</span>";
|
| 259 |
}
|
| 260 |
if (!empty($this['title'])) {
|
| 261 |
$output .= ' <label>'. t('!title :', array('!title' => $this['title'])) ."</label>\n";
|
| 262 |
}
|
| 263 |
$output .= '<input type="textfield" name="'. $this['post_name'] .'" " value="'. $this['value'] ."\" ";
|
| 264 |
$output .= drupal_attributes($this['attributes']) ." />\n";
|
| 265 |
|
| 266 |
if (!empty($this['description'])) {
|
| 267 |
$output .= ' <div class="description">'. $this['description'] ."</div>\n";
|
| 268 |
}
|
| 269 |
return $output;
|
| 270 |
}
|
| 271 |
}
|
| 272 |
|
| 273 |
|
| 274 |
|