| 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 |
* Register a callback for a $signal emitted by $class.
|
| 10 |
*
|
| 11 |
* @param $class The class to which you want to listen
|
| 12 |
* @param $signal The signal (event) to which you want to react
|
| 13 |
* @param $callback
|
| 14 |
* The callback that is invoked when $signal is emitted.
|
| 15 |
* Either a string containing the name of a function, an array of an object
|
| 16 |
* and a method name or an array of a class name and a method name
|
| 17 |
*/
|
| 18 |
function connect($class, $signal, $callback) {
|
| 19 |
static $connections;
|
| 20 |
|
| 21 |
// Return the stored connections. Only needed for the get_connections function
|
| 22 |
if (is_null($class) && is_null($signal) && is_null($callback)) {
|
| 23 |
return $connections;
|
| 24 |
}
|
| 25 |
|
| 26 |
if (is_string($class)) {
|
| 27 |
$connections[$class][$signal][] = $callback;
|
| 28 |
}
|
| 29 |
elseif (is_object($class) && $class instanceof DrupalObject) {
|
| 30 |
$class->addCallback($signal, $callback);
|
| 31 |
}
|
| 32 |
}
|
| 33 |
|
| 34 |
/**
|
| 35 |
* Get an array of registered connections for a class/signal combination.
|
| 36 |
*/
|
| 37 |
function get_connections($class, $signal = NULL) {
|
| 38 |
$connections = connect(NULL, NULL, NULL);
|
| 39 |
if (is_null($signal)) {
|
| 40 |
return isset($connections[$class]) ? $connections[$class] : array();
|
| 41 |
}
|
| 42 |
else {
|
| 43 |
return isset($connections[$class][$signal]) ? $connections[$class][$signal] : array();
|
| 44 |
}
|
| 45 |
}
|
| 46 |
|
| 47 |
class Form {
|
| 48 |
/**
|
| 49 |
* Load a form.
|
| 50 |
*
|
| 51 |
* This first checks whether there is POST data for the given form.
|
| 52 |
* If so, we load the built form from the cache.
|
| 53 |
*
|
| 54 |
* Otherwise, we check whether we have a cached, readily constructed skeleton cache
|
| 55 |
* for the form. If not, we create a new one.
|
| 56 |
* Then, the 'build' signal is emitted both for cached skeletons and newly created objects, so
|
| 57 |
* all dynamic actions should happen there instead of in __construct.
|
| 58 |
*
|
| 59 |
* @param $form_id
|
| 60 |
* The name of the form (class) to load.
|
| 61 |
*
|
| 62 |
* Additional arguments will be passed on to the build callback/method.
|
| 63 |
*/
|
| 64 |
static function load() {
|
| 65 |
$args = func_get_args();
|
| 66 |
$form_id = array_shift($args);
|
| 67 |
|
| 68 |
if (isset($_POST["$form_id/form_id"]) && $_POST["$form_id/form_id"] == $form_id
|
| 69 |
&& !empty($_POST["$form_id/form_build_id"])) {
|
| 70 |
$cached = cache_get('form_build:'. $_POST["$form_id/form_build_id"]);
|
| 71 |
$form = $cached->data;
|
| 72 |
call_user_func_array(array($form, 'build'), $args);
|
| 73 |
print 'build cached<br>';
|
| 74 |
}
|
| 75 |
|
| 76 |
// UNCOMMENT THE FOLLOWING LINES TO ENABLE THE SKELETON CACHE!
|
| 77 |
// It works, but is disabled right now.
|
| 78 |
|
| 79 |
/* elseif ($cached = cache_get("form_skeleton:$form_id")) {
|
| 80 |
$form = $cached->data;
|
| 81 |
call_user_func_array(array($form, 'build'), $args);
|
| 82 |
print 'skeleton cached<br>';
|
| 83 |
}
|
| 84 |
*/
|
| 85 |
|
| 86 |
else {
|
| 87 |
$form = new $form_id();
|
| 88 |
cache_set("form_skeleton:$form_id", $form);
|
| 89 |
call_user_func_array(array($form, 'build'), $args);
|
| 90 |
print 'not cached<br>';
|
| 91 |
}
|
| 92 |
return $form;
|
| 93 |
}
|
| 94 |
}
|
| 95 |
|
| 96 |
/**
|
| 97 |
* The class DrupalObject provides functionality that is widely known as
|
| 98 |
* the 'Observer' pattern or 'Signals and Slots'.
|
| 99 |
*
|
| 100 |
* Here, this is basically a simple callback mechanism that allows objects to talk
|
| 101 |
* with each other.
|
| 102 |
*
|
| 103 |
* Objects can invoke callbacks whenever they want and without caring whether there are
|
| 104 |
* actual callbacks registered for a certain signal.
|
| 105 |
*
|
| 106 |
* Callbacks can be registered either by calling the addCallback method of an object, or
|
| 107 |
* by using the connect() function, which allows to register callbacks for not yet instanced
|
| 108 |
* classes.
|
| 109 |
*
|
| 110 |
* @author Franz Heinzmann (Frando), http://unbiskant.org
|
| 111 |
* @see
|
| 112 |
* http://www.php.net/~helly/php/ext/spl/
|
| 113 |
* http://www.angrydonuts.com/what_if_fapi_were_oo
|
| 114 |
*/
|
| 115 |
abstract class DrupalObject {
|
| 116 |
private $_callbacks = array();
|
| 117 |
|
| 118 |
/**
|
| 119 |
* Constructor
|
| 120 |
*/
|
| 121 |
public function __construct() {
|
| 122 |
|
| 123 |
// Add all callbacks that are defined for the object.
|
| 124 |
foreach (get_connections(get_class($this)) as $signal => $callbacks) {
|
| 125 |
foreach ($callbacks as $callback) {
|
| 126 |
$this->addCallback($signal, $callback);
|
| 127 |
}
|
| 128 |
}
|
| 129 |
|
| 130 |
return $this;
|
| 131 |
}
|
| 132 |
|
| 133 |
/**
|
| 134 |
* Callback stuff
|
| 135 |
*/
|
| 136 |
|
| 137 |
/**
|
| 138 |
* Add a callback for a given signal.
|
| 139 |
*
|
| 140 |
* The callback can be anything callable (either a string containing the
|
| 141 |
* name of a function, an array of an object and a method name or an array
|
| 142 |
* of a class name and a method name).
|
| 143 |
*
|
| 144 |
* It will be called when $signal is emitted.
|
| 145 |
*/
|
| 146 |
public function addCallback($signal, $callback) {
|
| 147 |
$this->_callbacks[$signal][] = $callback;
|
| 148 |
}
|
| 149 |
|
| 150 |
public function removeCallback($signal, $callback = NULL) {
|
| 151 |
if (is_null($callback)) {
|
| 152 |
unset($this->_callbacks[$signal]);
|
| 153 |
}
|
| 154 |
else {
|
| 155 |
foreach ($this->_callbacks[$signal] as $key => $current_slot) {
|
| 156 |
if ($current_slot === $callback) {
|
| 157 |
unset($this->_callbacks[$signal][$key]);
|
| 158 |
}
|
| 159 |
}
|
| 160 |
}
|
| 161 |
}
|
| 162 |
|
| 163 |
/**
|
| 164 |
* Invoke all callbacks that are defined for a given signal.
|
| 165 |
*
|
| 166 |
* @param $signal
|
| 167 |
* The name of the signal to emit (= the name of the callback to invoke)
|
| 168 |
*
|
| 169 |
* This first calls the method $signal in $this (if it exists). Then,
|
| 170 |
* each additional callback that is registered for $signal is called.
|
| 171 |
*
|
| 172 |
* Additional parameters will be passed on to the callbacks.
|
| 173 |
*
|
| 174 |
* Internal callback will just get the passed arguments as arguments.
|
| 175 |
* External callbacks receive the object ($this) as the first argument, the name
|
| 176 |
* of the signal as second argument and then the passed additional arguments.
|
| 177 |
*/
|
| 178 |
public function invoke() {
|
| 179 |
$func_args = func_get_args();
|
| 180 |
$signal = array_shift($func_args);
|
| 181 |
|
| 182 |
// Check if the methods is defined in the current object.
|
| 183 |
if (method_exists($this, $signal)) {
|
| 184 |
call_user_func_array(array($this, $signal), $func_args);
|
| 185 |
}
|
| 186 |
|
| 187 |
// Check if additional (external) callbacks are registered
|
| 188 |
if (isset($this->_callbacks[$signal])) {
|
| 189 |
$args = array(&$this, $signal);
|
| 190 |
foreach ($func_args as $arg) {
|
| 191 |
$args[] = $arg;
|
| 192 |
}
|
| 193 |
foreach ($this->_callbacks[$signal] as $callback) {
|
| 194 |
if (is_callable($callback)) {
|
| 195 |
call_user_func_array($callback, $args);
|
| 196 |
}
|
| 197 |
}
|
| 198 |
}
|
| 199 |
}
|
| 200 |
}
|
| 201 |
|
| 202 |
function q($object) {
|
| 203 |
return new DrupalIterator($object);
|
| 204 |
}
|
| 205 |
|
| 206 |
/**
|
| 207 |
* DrupalElement is a class to represent an element in a tree.
|
| 208 |
*
|
| 209 |
* A DrupalElement basically differentiates between children and properties.
|
| 210 |
*
|
| 211 |
* Children can be set and accessed with the -> syntax (i.e. by using the normal PHP
|
| 212 |
* object property syntax):
|
| 213 |
* if it would be a regular PHP Array.
|
| 214 |
* @code
|
| 215 |
* $object=>some_child = new ...; // Any class that is derived from DrupalElement
|
| 216 |
* @endcode
|
| 217 |
*
|
| 218 |
* Properties can be set and accessed with the [] syntax (i.e. by treating a DrupalElement
|
| 219 |
* object as if it would be a regular PHP Array).
|
| 220 |
* @code
|
| 221 |
* $object['some_property'] = 'baz';
|
| 222 |
* @endcode
|
| 223 |
*
|
| 224 |
* All children of a DrupalElement have to be an instance of a class that is based on
|
| 225 |
* DrupalElement, otherwise, an exception is raised.
|
| 226 |
*
|
| 227 |
* Internally, this behaviour is realized by implementing ArrayAccess,
|
| 228 |
* an interface that is provided by the SPL and that allows to override the PHP array
|
| 229 |
* access syntax for an object.
|
| 230 |
*
|
| 231 |
* The foreach language construct is also overloaded. Instead of iterating over all
|
| 232 |
* public properties (default PHP behaviour when using an object with foreach), we
|
| 233 |
* can customize over what we want to iterate. This is realized by implementing
|
| 234 |
* the Iterator interface provided by the SPL.
|
| 235 |
*
|
| 236 |
* So, by default, foreach($object as $child_name => $child) iterates over all children
|
| 237 |
* of $object. Note that in PHP5, objects are always passed by reference, so
|
| 238 |
* $object[$child_name]->foo = 'bar' is equal to $child->foo = 'bar'.
|
| 239 |
*
|
| 240 |
* Now, DrupalElement provides several methods that allow to manipulate over which
|
| 241 |
* elements foreach iterates.
|
| 242 |
*
|
| 243 |
* All of these methods (and even some more) return $this, which makes them chainable.
|
| 244 |
* And yeah, this is similar to the jQuery way of doing things (jQuery was in fact a main
|
| 245 |
* inspiration for this whole iteration/traversable thing).
|
| 246 |
*
|
| 247 |
* @author Franz Heinzmann (Frando), http://unbiskant.org
|
| 248 |
* @see
|
| 249 |
* http://www.php.net/~helly/php/ext/spl/
|
| 250 |
* http://www.angrydonuts.com/what_if_fapi_were_oo
|
| 251 |
*/
|
| 252 |
abstract class DrupalElement extends DrupalObject implements ArrayAccess, Countable, IteratorAggregate {
|
| 253 |
|
| 254 |
// Contains all properties.
|
| 255 |
protected $_properties = array();
|
| 256 |
// Contains all children.
|
| 257 |
public $_children = array();
|
| 258 |
|
| 259 |
// Contains all descendants (children and grandchildren and grandgrandchildren etc.)
|
| 260 |
public $_descendants = array();
|
| 261 |
|
| 262 |
|
| 263 |
/**
|
| 264 |
* Constructor
|
| 265 |
*/
|
| 266 |
public function __construct($properties = array()) {
|
| 267 |
parent::__construct();
|
| 268 |
$this->set($properties);
|
| 269 |
|
| 270 |
$this['type'] = get_class($this);
|
| 271 |
$this['name'] = isset($this['name']) ? $this['name'] : $this['type'];
|
| 272 |
// By default, we're the only element, and thus the tree's root.
|
| 273 |
$this['treeRoot'] = $this;
|
| 274 |
// TODO $this['built'] = FALSE;
|
| 275 |
|
| 276 |
// Reset the selection.
|
| 277 |
// $this->resetSelection();
|
| 278 |
|
| 279 |
// Invoke 'construct'
|
| 280 |
$this->invoke('construct');
|
| 281 |
return $this;
|
| 282 |
}
|
| 283 |
|
| 284 |
/**
|
| 285 |
* Implement ArrayAccess to be able to access the object with PHP's normal Array syntax,
|
| 286 |
* which is used to access the element's properties.
|
| 287 |
*/
|
| 288 |
public function offsetSet($name, $value) {
|
| 289 |
$this->_properties[$name] = $value;
|
| 290 |
}
|
| 291 |
|
| 292 |
public function offsetGet($name) {
|
| 293 |
return $this->_properties[$name];
|
| 294 |
}
|
| 295 |
|
| 296 |
public function offsetExists($name) {
|
| 297 |
return isset($this->_properties[$name]);
|
| 298 |
}
|
| 299 |
|
| 300 |
public function offsetUnset($name) {
|
| 301 |
unset($this->_properties[$name]);
|
| 302 |
}
|
| 303 |
|
| 304 |
/**
|
| 305 |
* Magic methods to be able to access children with the -> syntax.
|
| 306 |
* $object->foo = new bar() is equal to $object->addChild('foo', new bar())
|
| 307 |
*/
|
| 308 |
public function __set($name, $value) {
|
| 309 |
$this->addChild($name, $value);
|
| 310 |
}
|
| 311 |
|
| 312 |
public function __get($name) {
|
| 313 |
return $this->_children[$name];
|
| 314 |
}
|
| 315 |
|
| 316 |
public function __isset($name) {
|
| 317 |
return isset($this->_children[$name]);
|
| 318 |
}
|
| 319 |
|
| 320 |
public function __unset($name) {
|
| 321 |
unset($this->_children[$name]);
|
| 322 |
}
|
| 323 |
|
| 324 |
/**
|
| 325 |
* Implement Countable
|
| 326 |
*/
|
| 327 |
public function count() {
|
| 328 |
return count($this->_children);
|
| 329 |
}
|
| 330 |
|
| 331 |
/**
|
| 332 |
* Implement IteratorAggregate
|
| 333 |
*/
|
| 334 |
public function getIterator() {
|
| 335 |
return new DrupalIterator($this);
|
| 336 |
}
|
| 337 |
|
| 338 |
/**
|
| 339 |
* Children management
|
| 340 |
*/
|
| 341 |
|
| 342 |
/**
|
| 343 |
* Add a child to the current element.
|
| 344 |
* All children must be an instance of a class that is based on DrupalElement,
|
| 345 |
* otherwise, an Exception is thrown.
|
| 346 |
*
|
| 347 |
* $object->addChild('foo', new bar()) is equal to $object['foo'] = new bar()
|
| 348 |
*/
|
| 349 |
public function addChild($name, $child) {
|
| 350 |
if (!$child instanceof DrupalElement) {
|
| 351 |
throw new Exception('DrupalElement::addChild(): Argument #2 is not a DrupalElement');
|
| 352 |
}
|
| 353 |
// Add the element to the current element's children (remind that objects are always copied by reference)
|
| 354 |
$this->_children[$name] = $child;
|
| 355 |
|
| 356 |
// Set basic properties for the child
|
| 357 |
$child->set(array(
|
| 358 |
'name' => $name,
|
| 359 |
'inTree' => TRUE,
|
| 360 |
'treeParent' => $this,
|
| 361 |
'treeRoot' => $this['treeRoot'],
|
| 362 |
));
|
| 363 |
|
| 364 |
// Add the element to the current element and its parents as a descendant
|
| 365 |
$this->addDescendantRecursive($child);
|
| 366 |
|
| 367 |
// Reset the current selection.
|
| 368 |
// $this->resetSelection();
|
| 369 |
// $child->resetSelection();
|
| 370 |
|
| 371 |
// Emit the 'childAdded' signal of the current element and pass the child as argument
|
| 372 |
$this->invoke('childAdded', $child);
|
| 373 |
// Emit the 'addedAsChild' signal of the child
|
| 374 |
$child->invoke('addedAsChild');
|
| 375 |
|
| 376 |
// Return the child to make addChild chainable
|
| 377 |
return $child;
|
| 378 |
}
|
| 379 |
|
| 380 |
protected function addDescendantRecursive($descendant) {
|
| 381 |
$this->_descendants[] = $descendant;
|
| 382 |
$this->invoke('descendantAdded', $descendant);
|
| 383 |
if (!$this->isTreeRoot()) {
|
| 384 |
$this->parent()->addDescendantRecursive($descendant);
|
| 385 |
}
|
| 386 |
}
|
| 387 |
|
| 388 |
/**
|
| 389 |
* Return the element's parent.
|
| 390 |
*/
|
| 391 |
public function parent() {
|
| 392 |
return $this['treeParent'];
|
| 393 |
}
|
| 394 |
|
| 395 |
/**
|
| 396 |
* Return the root element of the tree in which the element is.
|
| 397 |
*/
|
| 398 |
public function root() {
|
| 399 |
return $this['treeRoot'];
|
| 400 |
}
|
| 401 |
|
| 402 |
/**
|
| 403 |
* This adds $this plus it's parent plus it's parent's parent etc. to $selection, which is passed
|
| 404 |
* from element to parent to parent recursively.
|
| 405 |
*/
|
| 406 |
public function addParent(&$selection) {
|
| 407 |
$selection[] = $this;
|
| 408 |
if (!$this->isTreeRoot()) {
|
| 409 |
$this->parent()->addParent($selection);
|
| 410 |
}
|
| 411 |
}
|
| 412 |
|
| 413 |
/**
|
| 414 |
* This creates a 'path' from $start to $stop.
|
| 415 |
* With no args, it creates a path from the treeRoot to the current element,
|
| 416 |
* seperated by '/'.
|
| 417 |
*
|
| 418 |
* This, together with findByPath, is used to create the POST 'name's for form elements
|
| 419 |
* and then to assign the POST values to the correct elements.
|
| 420 |
*/
|
| 421 |
public function createPath($start = NULL, $stop = NULL) {
|
| 422 |
if (is_null($start)) {
|
| 423 |
$start = $this['treeRoot'];
|
| 424 |
}
|
| 425 |
if (is_null($stop)) {
|
| 426 |
$stop = $this;
|
| 427 |
}
|
| 428 |
$parents = array();
|
| 429 |
foreach (q($stop)->parents() as $parent) {
|
| 430 |
$parents[] = $parent['name'];
|
| 431 |
if ($parent === $start) {
|
| 432 |
break;
|
| 433 |
}
|
| 434 |
}
|
| 435 |
$parents = array_reverse($parents);
|
| 436 |
return implode('/', $parents) . '/' . $stop['name'];
|
| 437 |
}
|
| 438 |
|
| 439 |
/**
|
| 440 |
* This returns the element that has the path $path from the current element.
|
| 441 |
*/
|
| 442 |
public function findByPath($path) {
|
| 443 |
$path = explode('/', $path);
|
| 444 |
// TODO: this is ugly.
|
| 445 |
if ($this['name'] == $path[0]) {
|
| 446 |
array_shift($path);
|
| 447 |
}
|
| 448 |
return $this->findRecursively($path);
|
| 449 |
}
|
| 450 |
|
| 451 |
public function findRecursively($parts) {
|
| 452 |
$name = array_shift($parts);
|
| 453 |
if (!isset($this->$name)) {
|
| 454 |
return FALSE;
|
| 455 |
}
|
| 456 |
if (count($parts)) {
|
| 457 |
return $this->$name->findRecursively($parts);
|
| 458 |
}
|
| 459 |
else {
|
| 460 |
return $this->$name;
|
| 461 |
}
|
| 462 |
}
|
| 463 |
|
| 464 |
/**
|
| 465 |
* Mixed functions
|
| 466 |
*/
|
| 467 |
public function isTreeRoot() {
|
| 468 |
return ($this['treeRoot'] === $this);
|
| 469 |
}
|
| 470 |
public function hasChildren() {
|
| 471 |
return !empty($this->_children);
|
| 472 |
}
|
| 473 |
public function __toString() {
|
| 474 |
return $this['type'] .' '. $this['name'];
|
| 475 |
}
|
| 476 |
|
| 477 |
/**
|
| 478 |
* Pass in an array of key/value pairs to be set as object properties.
|
| 479 |
*/
|
| 480 |
public function set($name, $value = NULL) {
|
| 481 |
if (!is_array($name)) {
|
| 482 |
$this[$name] = isset($value) ? $value : TRUE;
|
| 483 |
}
|
| 484 |
else {
|
| 485 |
foreach ($name as $name => $value) {
|
| 486 |
$this[$name] = $value;
|
| 487 |
}
|
| 488 |
}
|
| 489 |
return $this;
|
| 490 |
}
|
| 491 |
|
| 492 |
public function get($name = NULL) {
|
| 493 |
if (!isset($name)) {
|
| 494 |
return $this->_properties;
|
| 495 |
}
|
| 496 |
else {
|
| 497 |
return $this->_properties[$name];
|
| 498 |
}
|
| 499 |
}
|
| 500 |
|
| 501 |
}
|
| 502 |
|
| 503 |
class DrupalIterator implements Iterator, Countable {
|
| 504 |
// Contains the current 'selection' of element over which foreach iterates
|
| 505 |
public $selection = array();
|
| 506 |
|
| 507 |
// Needed for the Iterator implentation.
|
| 508 |
private $selectionValid;
|
| 509 |
|
| 510 |
function __construct($object) {
|
| 511 |
$this->selection = array($object);
|
| 512 |
return $this;
|
| 513 |
}
|
| 514 |
|
| 515 |
public function __call($method, $args) {
|
| 516 |
foreach ($this->selection as $item) {
|
| 517 |
if (method_exists($item, $method)) {
|
| 518 |
call_user_func_array(array($item, $method), $args);
|
| 519 |
}
|
| 520 |
}
|
| 521 |
return $this;
|
| 522 |
}
|
| 523 |
/**
|
| 524 |
* Implement Iterator to overload the foreach language construct.
|
| 525 |
*
|
| 526 |
* When an instance of DrupalElement is used in a foreach loop, it iterates
|
| 527 |
* over the internal, protected $selection property. By default, $selection contains
|
| 528 |
* all children of the object. See the Traversing methods on how to manipulate $selection.
|
| 529 |
*/
|
| 530 |
|
| 531 |
/* (The following is just the standard implentation of the Iterator interface) */
|
| 532 |
/**
|
| 533 |
* Return the array "pointer" to the first element
|
| 534 |
* PHP's reset() returns false if the array has no elements
|
| 535 |
*/
|
| 536 |
function rewind() {
|
| 537 |
if (reset($this->selection) !== FALSE) {
|
| 538 |
$this->selectionValid = TRUE;
|
| 539 |
}
|
| 540 |
else {
|
| 541 |
$this->selectionValid = FALSE;
|
| 542 |
}
|
| 543 |
|
| 544 |
}
|
| 545 |
|
| 546 |
/**
|
| 547 |
* Return the current array element
|
| 548 |
*/
|
| 549 |
function current() {
|
| 550 |
return current($this->selection);
|
| 551 |
}
|
| 552 |
|
| 553 |
/**
|
| 554 |
* Return the key of the current array element
|
| 555 |
*/
|
| 556 |
function key() {
|
| 557 |
return key($this->selection);
|
| 558 |
}
|
| 559 |
|
| 560 |
/**
|
| 561 |
* Move forward by one
|
| 562 |
* PHP's next() returns false if there are no more elements
|
| 563 |
*/
|
| 564 |
function next() {
|
| 565 |
if (next($this->selection) !== FALSE) {
|
| 566 |
$this->selectionValid = TRUE;
|
| 567 |
}
|
| 568 |
else {
|
| 569 |
$this->selectionValid = FALSE;
|
| 570 |
// Reset the selection at the end of a foreach loop.
|
| 571 |
// $this->resetSelection();
|
| 572 |
}
|
| 573 |
}
|
| 574 |
|
| 575 |
/**
|
| 576 |
* Is the current element valid?
|
| 577 |
*/
|
| 578 |
function valid(){
|
| 579 |
return $this->selectionValid;
|
| 580 |
}
|
| 581 |
|
| 582 |
/**
|
| 583 |
* Implement Countable
|
| 584 |
*/
|
| 585 |
public function count() {
|
| 586 |
return count($this->selection);
|
| 587 |
}
|
| 588 |
|
| 589 |
/**
|
| 590 |
* Traversing
|
| 591 |
*
|
| 592 |
* Each of these functions sets the internal, private $selection property to some array
|
| 593 |
* of children/parents/etc.
|
| 594 |
* The foreach language construct is overloaded and when used
|
| 595 |
* with a DrupalElement object, it iterates over the elements in $selection.
|
| 596 |
*
|
| 597 |
* As $this is returned, these functions are chainable (jQuery style).
|
| 598 |
*/
|
| 599 |
|
| 600 |
public function selection() {
|
| 601 |
return $this->selection;
|
| 602 |
}
|
| 603 |
|
| 604 |
public function add($element) {
|
| 605 |
$this->selection[] = $element;
|
| 606 |
return $this;
|
| 607 |
}
|
| 608 |
|
| 609 |
public function end() {
|
| 610 |
return $this->resetSelection();
|
| 611 |
}
|
| 612 |
|
| 613 |
public function resetSelection() {
|
| 614 |
$this->selection = array($this);
|
| 615 |
return $this;
|
| 616 |
}
|
| 617 |
|
| 618 |
/**
|
| 619 |
* Select all children.
|
| 620 |
*/
|
| 621 |
public function children($filter = NULL) {
|
| 622 |
// TODO! This is just that everything basically works, but children() has to be chainable too
|
| 623 |
// $this->selection = array_values($this->_children);
|
| 624 |
// return $this;
|
| 625 |
// TODO! We need to 'close' the selection if we want this to be chainable.
|
| 626 |
$selection = array();
|
| 627 |
foreach ($this->selection as $element) {
|
| 628 |
$selection = array_merge($selection, array_values($element->_children));
|
| 629 |
}
|
| 630 |
array_unique($selection);
|
| 631 |
$this->selection = $selection;
|
| 632 |
|
| 633 |
if (isset($filter)) {
|
| 634 |
$this->filter($filter);
|
| 635 |
}
|
| 636 |
return $this;
|
| 637 |
}
|
| 638 |
|
| 639 |
/**
|
| 640 |
* Select all parents of the current element
|
| 641 |
*/
|
| 642 |
public function parents($filter = NULL) {
|
| 643 |
$selection = array();
|
| 644 |
foreach ($this->selection as $element) {
|
| 645 |
if (!$element->isTreeRoot()) {
|
| 646 |
$element->parent()->addParent($selection);
|
| 647 |
}
|
| 648 |
}
|
| 649 |
array_unique($selection);
|
| 650 |
$this->selection = $selection;
|
| 651 |
|
| 652 |
if (isset($filter)) {
|
| 653 |
$this->filter($filter);
|
| 654 |
}
|
| 655 |
return $this;
|
| 656 |
}
|
| 657 |
|
| 658 |
/**
|
| 659 |
* Select all siblings of the current element
|
| 660 |
*/
|
| 661 |
public function siblings($filter = NULL) {
|
| 662 |
$selection = array();
|
| 663 |
foreach ($this->selection as $element) {
|
| 664 |
$children = $element->parent()->_children;
|
| 665 |
unset($children[$element['name']]);
|
| 666 |
$selection = array_merge($selection, array_values($children));
|
| 667 |
// foreach ($element->parent()->_children as $name => $child) {
|
| 668 |
// if ($name != $element['name']) {
|
| 669 |
// $selection[] = $child;
|
| 670 |
// }
|
| 671 |
// }
|
| 672 |
}
|
| 673 |
|
| 674 |
$this->selection = $selection;
|
| 675 |
|
| 676 |
if (isset($filter)) {
|
| 677 |
$this->filter($filter);
|
| 678 |
}
|
| 679 |
|
| 680 |
return $this;
|
| 681 |
}
|
| 682 |
|
| 683 |
/**
|
| 684 |
* Select all descendants of the current element
|
| 685 |
*/
|
| 686 |
public function descendants($filter = NULL) {
|
| 687 |
$selection = array();
|
| 688 |
foreach ($this->selection as $element) {
|
| 689 |
$selection = array_merge($selection, $element->_descendants);
|
| 690 |
}
|
| 691 |
$this->selection = $selection;
|
| 692 |
|
| 693 |
if (isset($filter)) {
|
| 694 |
$this->filter($filter);
|
| 695 |
}
|
| 696 |
|
| 697 |
return $this;
|
| 698 |
}
|
| 699 |
|
| 700 |
/**
|
| 701 |
* Select all descendants of the current element and the current element itself
|
| 702 |
*/
|
| 703 |
public function all() {
|
| 704 |
return $this->descendants()->add($this);
|
| 705 |
}
|
| 706 |
|
| 707 |
/**
|
| 708 |
* Filter the current selection (dummy function, not working)
|
| 709 |
*/
|
| 710 |
public function filter($expression) {
|
| 711 |
$selection = array();
|
| 712 |
// reg ex: 1
|
| 713 |
if (!preg_match('!
|
| 714 |
([.#]{1}) # Either . for class names (types) or # for properties
|
| 715 |
([a-zA-Z_]+) # $identifier
|
| 716 |
(=([a-zA-Z_0-9]+))? # optionally, =$value
|
| 717 |
!', $expression, $matches)
|
| 718 |
) {
|
| 719 |
return;
|
| 720 |
}
|
| 721 |
$identifier = $matches[2];
|
| 722 |
switch ($matches[1]) {
|
| 723 |
case '.':
|
| 724 |
$type = 'type';
|
| 725 |
break;
|
| 726 |
case '#':
|
| 727 |
$type = 'property';
|
| 728 |
if (isset($matches[4])) {
|
| 729 |
$value = $matches[4];
|
| 730 |
}
|
| 731 |
}
|
| 732 |
foreach($this->selection as $element) {
|
| 733 |
switch ($type) {
|
| 734 |
case 'type':
|
| 735 |
if ($element instanceof $identifier) {
|
| 736 |
$selection[] = $element;
|
| 737 |
}
|
| 738 |
break;
|
| 739 |
case 'property':
|
| 740 |
if (isset($element[$identifier])) {
|
| 741 |
if ( (!isset($value) && $element[$identifier]) || (isset($value) && $element[$identifier] == $value)) {
|
| 742 |
$selection[] = $element;
|
| 743 |
}
|
| 744 |
}
|
| 745 |
break;
|
| 746 |
}
|
| 747 |
}
|
| 748 |
$this->selection = $selection;
|
| 749 |
return $this;
|
| 750 |
}
|
| 751 |
|
| 752 |
// This should just be part of filter and f with some syntax like .type
|
| 753 |
public function filterByType($type) {
|
| 754 |
$selection = array();
|
| 755 |
foreach($this->selection as $element) {
|
| 756 |
if ($element instanceof $type) {
|
| 757 |
$selection[] = $element;
|
| 758 |
}
|
| 759 |
}
|
| 760 |
$this->selection = $selection;
|
| 761 |
return $this;
|
| 762 |
}
|
| 763 |
|
| 764 |
/**
|
| 765 |
* A query engine should live here. Imagine something like fQuery.
|
| 766 |
* This should allow you to select elements with a CSS-like syntax
|
| 767 |
* and then iterate over them with foreach.
|
| 768 |
*/
|
| 769 |
public function f($query) {
|
| 770 |
}
|
| 771 |
|
| 772 |
}
|