Overview

Packages

  • Xml
    • Element

Classes

  • Horde_Xml_Element
  • Horde_Xml_Element_Exception
  • Horde_Xml_Element_List
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Portions Copyright 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
  4:  * Copyright 2007-2012 Horde LLC (http://www.horde.org/)
  5:  *
  6:  * @author   Chuck Hagenbuch <chuck@horde.org>
  7:  * @license  http://www.horde.org/licenses/bsd BSD
  8:  * @category Horde
  9:  * @package  Xml_Element
 10:  */
 11: 
 12: /**
 13:  * Wraps a DOMElement allowing for SimpleXML-like access to attributes.
 14:  *
 15:  * @method mixed TAGNAME() To get the un-wrapped value of a node, use
 16:  * method syntax ($xml_element->tagname()). This will return the
 17:  * string value of the tag if it is a single tag, an array of
 18:  * Horde_Xml_Element objects if there are multiple tags, or null if
 19:  * the tag does not exist.
 20:  *
 21:  * @author   Chuck Hagenbuch <chuck@horde.org>
 22:  * @license  http://www.horde.org/licenses/bsd BSD
 23:  * @category Horde
 24:  * @package  Xml_Element
 25:  */
 26: class Horde_Xml_Element implements ArrayAccess
 27: {
 28:     /**
 29:      * @var array
 30:      */
 31:     protected static $_namespaces = array(
 32:         'opensearch' => 'http://a9.com/-/spec/opensearchrss/1.0/',
 33:         'atom' => 'http://www.w3.org/2005/Atom',
 34:         'rss' => 'http://blogs.law.harvard.edu/tech/rss',
 35:         'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns',
 36:     );
 37: 
 38:     /**
 39:      * Get the full version of a namespace prefix
 40:      *
 41:      * Looks up a prefix (atom:, etc.) in the list of registered
 42:      * namespaces and returns the full namespace URI if
 43:      * available. Returns the prefix, unmodified, if it's not
 44:      * registered.
 45:      *
 46:      * @return string
 47:      */
 48:     public static function lookupNamespace($prefix)
 49:     {
 50:         return isset(self::$_namespaces[$prefix]) ?
 51:             self::$_namespaces[$prefix] :
 52:             $prefix;
 53:     }
 54: 
 55:     /**
 56:      * Add a namespace and prefix to the registered list
 57:      *
 58:      * Takes a prefix and a full namespace URI and adds them to the
 59:      * list of registered namespaces for use by
 60:      * Horde_Xml_Element::lookupNamespace().
 61:      *
 62:      * @param string $prefix The namespace prefix
 63:      * @param string $namespaceURI The full namespace URI
 64:      */
 65:     public static function registerNamespace($prefix, $namespaceURI)
 66:     {
 67:         self::$_namespaces[$prefix] = $namespaceURI;
 68:     }
 69: 
 70:     /**
 71:      * @var DOMElement
 72:      */
 73:     protected $_element;
 74: 
 75:     /**
 76:      * A string representation of the element, used when
 77:      * serializing/unserializing.
 78:      *
 79:      * @var string
 80:      */
 81:     protected $_serialized;
 82: 
 83:     /**
 84:      * @var Horde_Xml_Element
 85:      */
 86:     protected $_parentElement;
 87: 
 88:     /**
 89:      * @var array
 90:      */
 91:     protected $_children = null;
 92: 
 93:     /**
 94:      * @var boolean
 95:      */
 96:     protected $_appended = true;
 97: 
 98:     /**
 99:      * Horde_Xml_Element constructor.
100:      *
101:      * @param DOMElement | Horde_Xml_Element | string $element  The DOM element,
102:      * pre-existing Horde_Xml_Element, or XML string that we're encapsulating.
103:      */
104:     public function __construct($element)
105:     {
106:         $this->_element = $element;
107:         $this->__wakeup();
108:     }
109: 
110:     /**
111:      * Get a DOM representation of the element
112:      *
113:      * Returns the underlying DOM object, which can then be
114:      * manipulated with full DOM methods.
115:      *
116:      * @return DOMDocument
117:      */
118:     public function getDom()
119:     {
120:         return $this->_element;
121:     }
122: 
123:     /**
124:      * Update the object from a DOM element
125:      *
126:      * Take a DOMElement object, which may be originally from a call
127:      * to getDom() or may be custom created, and use it as the
128:      * DOM tree for this Horde_Xml_Element.
129:      *
130:      * @param DOMElement $element
131:      */
132:     public function setDom(DOMElement $element)
133:     {
134:         $this->_element = $this->_element->ownerDocument->importNode($element, true);
135:     }
136: 
137:     /**
138:      * Add child elements and attributes to this element from a simple
139:      * key => value hash. Keys can be:
140:      *
141:      *   ElementName               -> <$ElementName> will be appended with
142:      *                                a value of $value
143:      *   #AttributeName            -> An attribute $AttributeName will be
144:      *                                added to this element with a value
145:      *                                of $value
146:      *   ElementName#AttributeName -> <$ElementName> will be appended to this
147:      *                                element if it doesn't already exist,
148:      *                                and have its attribute $AttributeName
149:      *                                set to $value
150:      *
151:      * @param $array Hash to import into this element.
152:      */
153:     public function fromArray($array)
154:     {
155:         foreach ($array as $key => $value) {
156:             $element = null;
157:             $attribute = null;
158: 
159:             $hash_position = strpos($key, '#');
160:             if ($hash_position === false) {
161:                 $element = $key;
162:             } elseif ($hash_position === 0) {
163:                 $attribute = substr($key, 1);
164:             } else {
165:                 list($element, $attribute) = explode('#', $key, 2);
166:             }
167: 
168:             if (!is_null($element)) {
169:                 if (!is_null($attribute)) {
170:                     $this->{$element}[$attribute] = $value;
171:                 } else {
172:                     if (is_array($value)) {
173:                         // Detect numeric arrays and treat them as multiple
174:                         // instances of the same key.
175:                         $firstKey = key($value);
176:                         if ($firstKey === 0) {
177:                             if (strpos($element, ':') !== false) {
178:                                 list($ns) = explode(':', $element, 2);
179:                                 $baseNode = $this->_element->ownerDocument->createElementNS(Horde_Xml_Element::lookupNamespace($ns), $element);
180:                             } else {
181:                                 $baseNode = $this->_element->ownerDocument->createElement($element);
182:                             }
183: 
184:                             foreach ($value as $v) {
185:                                 $node = $baseNode->cloneNode();
186:                                 if (is_array($v)) {
187:                                     $e = new Horde_Xml_Element($node);
188:                                     $e->fromArray($v);
189:                                 } else {
190:                                     $node->nodeValue = $v;
191:                                     $e = new Horde_Xml_Element($node);
192:                                 }
193:                                 $this->appendChild($e);
194:                             }
195:                         } else {
196:                             $this->$element->fromArray($value);
197:                         }
198:                     } else {
199:                         $this->$element = $value;
200:                     }
201:                 }
202:             } elseif (!is_null($attribute)) {
203:                 $this[$attribute] = $value;
204:             }
205:         }
206:     }
207: 
208:     /**
209:      * Append a child node to this element.
210:      *
211:      * @param Horde_Xml_Element $element The element to append.
212:      */
213:     public function appendChild(Horde_Xml_Element $element)
214:     {
215:         $element->setParent($this);
216:         $element->_ensureAppended();
217:         $this->_expireCachedChildren();
218:     }
219: 
220:     /**
221:      * Get an XML string representation of this element
222:      *
223:      * Returns a string of this element's XML, including the XML
224:      * prologue.
225:      *
226:      * @return string
227:      */
228:     public function saveXml($formatted = false)
229:     {
230:         // Return a complete document including XML prologue.
231:         $doc = new DOMDocument($this->_element->ownerDocument->version,
232:                                $this->_element->ownerDocument->actualEncoding);
233:         $doc->formatOutput = $formatted;
234:         $doc->appendChild($doc->importNode($this->_element, true));
235:         return $doc->saveXML();
236:     }
237: 
238:     /**
239:      * Get the XML for only this element
240:      *
241:      * Returns a string of this element's XML without prologue.
242:      *
243:      * @return string
244:      */
245:     public function saveXmlFragment($formatted = false)
246:     {
247:         $oldFormatted = $this->_element->ownerDocument->formatOutput;
248:         $this->_element->ownerDocument->formatOutput = $formatted;
249:         $xml = $this->_element->ownerDocument->saveXML($this->_element);
250:         $this->_element->ownerDocument->formatOutput = $oldFormatted;
251:         return $xml;
252:     }
253: 
254:     /**
255:      * Unserialization handler; handles $this->_element being an instance of
256:      * DOMElement or Horde_Xml_Element, or parses it as an XML string.
257:      */
258:     public function __wakeup()
259:     {
260:         if ($this->_element instanceof DOMElement) {
261:             return true;
262:         }
263: 
264:         if ($this->_element instanceof Horde_Xml_Element) {
265:             $this->_element = $this->_element->getDom();
266:             return true;
267:         }
268: 
269:         if ($this->_serialized) {
270:             $this->_element = $this->_serialized;
271:             $this->_serialized = null;
272:         }
273: 
274:         if (is_string($this->_element)) {
275:             $doc = new DOMDocument();
276:             $doc->preserveWhiteSpace = false;
277: 
278:             $extract = false;
279:             if (substr($this->_element, 0, 5) != '<?xml') {
280:                 $extract = true;
281:                 $preamble = '<?xml version="1.0" encoding="UTF-8" ?><root ';
282:                 foreach (self::$_namespaces as $prefix => $nsUri) {
283:                     $preamble .= " xmlns:$prefix=\"$nsUri\"";
284:                 }
285:                 $preamble .= '>';
286:                 $this->_element = $preamble . $this->_element . '</root>';
287:             }
288: 
289:             $loaded = @$doc->loadXML($this->_element);
290:             if (!$loaded) {
291:                 throw new Horde_Xml_Element_Exception('DOMDocument cannot parse XML: ', error_get_last());
292:             }
293: 
294:             if ($extract) {
295:                 $newDoc = new DOMDocument();
296:                 $this->_element = $newDoc->importNode($doc->documentElement->childNodes->item(0), true);
297:             } else {
298:                 $this->_element = $doc->documentElement;
299:             }
300: 
301:             return true;
302:         }
303: 
304:         throw new InvalidArgumentException('Horde_Xml_Element initialization value must be a DOMElement, a Horde_Xml_Element, or a non-empty string; '
305:                                            . (gettype($this->_element) == 'object' ? get_class($this->_element) : gettype($this->_element))
306:                                            . ' given');
307:     }
308: 
309:     /**
310:      * Prepare for serialization
311:      *
312:      * @return array
313:      */
314:     public function __sleep()
315:     {
316:         $this->_serialized = $this->saveXml();
317:         return array('_serialized');
318:     }
319: 
320:     /**
321:      * Map variable access onto the underlying entry representation.
322:      *
323:      * Get-style access returns a Horde_Xml_Element representing the
324:      * child element accessed. To get string values, use method syntax
325:      * with the __call() overriding.
326:      *
327:      * @param string $var The property to access.
328:      * @return mixed
329:      */
330:     public function __get($var)
331:     {
332:         $nodes = $this->_children($var);
333:         $length = count($nodes);
334: 
335:         if ($length == 1) {
336:             if ($nodes[0] instanceof Horde_Xml_Element) {
337:                 return $nodes[0];
338:             }
339:             return new Horde_Xml_Element($nodes[0]);
340:         } elseif ($length > 1) {
341:             if ($nodes[0] instanceof Horde_Xml_Element) {
342:                 return $nodes;
343:             }
344:             return array_map(create_function('$e', 'return new Horde_Xml_Element($e);'), $nodes);
345:         } else {
346:             // When creating anonymous nodes for __set chaining, don't
347:             // call appendChild() on them. Instead we pass the current
348:             // element to them as an extra reference; the child is
349:             // then responsible for appending itself when it is
350:             // actually set. This way "if ($foo->bar)" doesn't create
351:             // a phantom "bar" element in our tree.
352:             if (strpos($var, ':') !== false) {
353:                 list($ns, $elt) = explode(':', $var, 2);
354:                 $node = $this->_element->ownerDocument->createElementNS(Horde_Xml_Element::lookupNamespace($ns), $elt);
355:             } else {
356:                 $node = $this->_element->ownerDocument->createElement($var);
357:             }
358:             $node = new Horde_Xml_Element($node);
359:             $node->setParent($this);
360:             return $node;
361:         }
362:     }
363: 
364:     /**
365:      * Map variable sets onto the underlying entry representation.
366:      *
367:      * @param string $var The property to change.
368:      * @param string $val The property's new value.
369:      */
370:     public function __set($var, $val)
371:     {
372:         if (!is_scalar($val)) {
373:             throw new InvalidArgumentException('Element values must be scalars, ' . gettype($val) . ' given');
374:         }
375: 
376:         $this->_ensureAppended();
377: 
378:         $nodes = $this->_children($var);
379:         if (!$nodes) {
380:             if (strpos($var, ':') !== false) {
381:                 list($ns) = explode(':', $var, 2);
382:                 $node = $this->_element->ownerDocument->createElementNS(Horde_Xml_Element::lookupNamespace($ns), $var, $val);
383:                 $this->_element->appendChild($node);
384:             } else {
385:                 $node = $this->_element->ownerDocument->createElement($var, $val);
386:                 $this->_element->appendChild($node);
387:             }
388: 
389:             $this->_expireCachedChildren();
390:         } elseif (count($nodes) > 1) {
391:             throw new Horde_Xml_Element_Exception('Cannot set the value of multiple nodes simultaneously.');
392:         } else {
393:             $nodes[0]->nodeValue = $val;
394:         }
395:     }
396: 
397:     /**
398:      * Map isset calls onto the underlying entry representation.
399:      */
400:     public function __isset($var)
401:     {
402:         return (boolean)$this->_children($var);
403:     }
404: 
405:     /**
406:      * Get the value of an element with method syntax.
407:      *
408:      * Map method calls to get the string value of the requested
409:      * element. If there are multiple elements that match, this will
410:      * return an array of those objects.
411:      *
412:      * @param string $var The element to get the string value of.
413:      *
414:      * @return mixed The node's value, null, or an array of nodes.
415:      */
416:     public function __call($var, $unused)
417:     {
418:         $nodes = $this->_children($var);
419: 
420:         if (!$nodes) {
421:             return null;
422:         } elseif (count($nodes) > 1) {
423:             if ($nodes[0] instanceof Horde_Xml_Element) {
424:                 return $nodes;
425:             }
426:             return array_map(create_function('$e', 'return new Horde_Xml_Element($e);'), $nodes);
427:         } else {
428:             if ($nodes[0] instanceof Horde_Xml_Element) {
429:                 return (string)$nodes[0];
430:             } else {
431:                 return $nodes[0]->nodeValue;
432:             }
433:         }
434:     }
435: 
436:     /**
437:      * Remove all children matching $var.
438:      */
439:     public function __unset($var)
440:     {
441:         $nodes = $this->_children($var);
442:         foreach ($nodes as $node) {
443:             $parent = $node->parentNode;
444:             $parent->removeChild($node);
445:         }
446: 
447:         $this->_expireCachedChildren();
448:     }
449: 
450:     /**
451:      * Returns the nodeValue of this element when this object is used
452:      * in a string context.
453:      *
454:      * @internal
455:      */
456:     public function __toString()
457:     {
458:         return $this->_element->nodeValue;
459:     }
460: 
461:     /**
462:      * Set the parent element of this object to another
463:      * Horde_Xml_Element.
464:      *
465:      * @internal
466:      */
467:     public function setParent(Horde_Xml_Element $element)
468:     {
469:         $this->_parentElement = $element;
470:         $this->_appended = false;
471:     }
472: 
473:     /**
474:      * Appends this element to its parent if necessary.
475:      *
476:      * @internal
477:      */
478:     protected function _ensureAppended()
479:     {
480:         if (!$this->_appended) {
481:             $parentDom = $this->_parentElement->getDom();
482:             if (!$parentDom->ownerDocument->isSameNode($this->_element->ownerDocument)) {
483:                 $this->_element = $parentDom->ownerDocument->importNode($this->_element, true);
484:             }
485: 
486:             $parentDom->appendChild($this->_element);
487:             $this->_appended = true;
488:             $this->_parentElement->_ensureAppended();
489:         }
490:     }
491: 
492:     /**
493:      * Finds children with tagnames matching $var
494:      *
495:      * Similar to SimpleXML's children() method.
496:      *
497:      * @param string Tagname to match, can be either namespace:tagName or just tagName.
498:      * @return array
499:      */
500:     protected function _children($var)
501:     {
502:         if (is_null($this->_children)) {
503:             $this->_cacheChildren();
504:         }
505: 
506:         // Honor any explicit getters. Because Horde_Xml_Element has a __call()
507:         // method, is_callable returns true on every method name. Use
508:         // method_exists instead.
509:         $varMethod = 'get' . ucfirst($var);
510:         if (method_exists($this, $varMethod)) {
511:             $children = call_user_func(array($this, $varMethod));
512:             if (is_null($children)) {
513:                 $this->_children[$var] = array();
514:             } elseif (!is_array($children)) {
515:                 $this->_children[$var] = array($children);
516:             } else {
517:                 $this->_children[$var] = $children;
518:             }
519:         }
520: 
521:         if (!isset($this->_children[$var])) {
522:             $this->_children[$var] = array();
523:         }
524: 
525:         return $this->_children[$var];
526:     }
527: 
528:     /**
529:      * Build a cache of child nodes.
530:      */
531:     protected function _cacheChildren()
532:     {
533:         foreach ($this->_element->childNodes as $child) {
534:             if (!isset($this->_children[$child->localName]))
535:                 $this->_children[$child->localName] = array();
536:             $this->_children[$child->localName][] = $child;
537: 
538:             if ($child->prefix) {
539:                 if (!isset($this->_children[$child->prefix . ':' . $child->localName]))
540:                     $this->_children[$child->prefix . ':' . $child->localName] = array();
541:                 $this->_children[$child->prefix . ':' . $child->localName][] = $child;
542:             }
543:         }
544:     }
545: 
546:     /**
547:      * Expire cached children.
548:      */
549:     protected function _expireCachedChildren()
550:     {
551:         $this->_children = null;
552:     }
553: 
554:     /**
555:      * Required by the ArrayAccess interface.
556:      *
557:      * @internal
558:      */
559:     public function offsetExists($offset)
560:     {
561:         if (strpos($offset, ':') !== false) {
562:             list($ns, $attr) = explode(':', $offset, 2);
563:             return $this->_element->hasAttributeNS(Horde_Xml_Element::lookupNamespace($ns), $attr);
564:         } else {
565:             return $this->_element->hasAttribute($offset);
566:         }
567:     }
568: 
569:     /**
570:      * Required by the ArrayAccess interface.
571:      *
572:      * @internal
573:      */
574:     public function offsetGet($offset)
575:     {
576:         if (strpos($offset, ':') !== false) {
577:             list($ns, $attr) = explode(':', $offset, 2);
578:             return $this->_element->getAttributeNS(Horde_Xml_Element::lookupNamespace($ns), $attr);
579:         } else {
580:             return $this->_element->getAttribute($offset);
581:         }
582:     }
583: 
584:     /**
585:      * Required by the ArrayAccess interface.
586:      *
587:      * @internal
588:      */
589:     public function offsetSet($offset, $value)
590:     {
591:         if (!is_scalar($value)) {
592:             throw new InvalidArgumentException('Element values must be scalars, ' . gettype($value) . ' given');
593:         }
594: 
595:         $this->_ensureAppended();
596: 
597:         if (strpos($offset, ':') !== false) {
598:             list($ns) = explode(':', $offset, 2);
599:             $result = $this->_element->setAttributeNS(Horde_Xml_Element::lookupNamespace($ns), $offset, $value);
600:         } else {
601:             $result = $this->_element->setAttribute($offset, $value);
602:         }
603: 
604:         if ($result) {
605:             $this->_expireCachedChildren();
606:             return true;
607:         } else {
608:             return false;
609:         }
610:     }
611: 
612:     /**
613:      * Required by the ArrayAccess interface.
614:      *
615:      * @internal
616:      */
617:     public function offsetUnset($offset)
618:     {
619:         if (strpos($offset, ':') !== false) {
620:             list($ns, $attr) = explode(':', $offset, 2);
621:             $result = $this->_element->removeAttributeNS(Horde_Xml_Element::lookupNamespace($ns), $attr);
622:         } else {
623:             $result = $this->_element->removeAttribute($offset);
624:         }
625: 
626:         if ($result) {
627:             $this->_expireCachedChildren();
628:             return true;
629:         } else {
630:             return false;
631:         }
632:     }
633: 
634: }
635: 
API documentation generated by ApiGen