/[drupal]/contributions/sandbox/frando/fapi4/classes.inc
ViewVC logotype

Contents of /contributions/sandbox/frando/fapi4/classes.inc

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.8 - (show annotations) (download) (as text)
Wed Sep 5 13:37:08 2007 UTC (2 years, 2 months ago) by frando
Branch: MAIN
CVS Tags: HEAD
Changes since 1.7: +261 -163 lines
File MIME type: text/x-php
FAPI4 at Froscon: Move all the Iterator and traversing stuff out of the DrupalElement class into a DrupalIterator class. Makes the code much more readable and understandable.
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 }

  ViewVC Help
Powered by ViewVC 1.1.2