PageRenderTime 48ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/src/FluentDOM/Core.php

http://github.com/ThomasWeinert/FluentDOM
PHP | 1000 lines | 548 code | 61 blank | 391 comment | 87 complexity | 8d4478dac7c17e3252a668f04a2483bb MD5 | raw file
  1. <?php
  2. /**
  3. * FluentDOMCore implements the core and interface functions for FluentDOM
  4. *
  5. * @version $Id$
  6. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  7. * @copyright Copyright (c) 2009 Bastian Feder, Thomas Weinert
  8. *
  9. * @package FluentDOM
  10. */
  11. /**
  12. * Include the external iterator class.
  13. */
  14. require_once(dirname(__FILE__).'/Iterator.php');
  15. /**
  16. * Include the loader interface.
  17. */
  18. require_once(dirname(__FILE__).'/Loader.php');
  19. /**
  20. * Include the handler class.
  21. */
  22. require_once(dirname(__FILE__).'/Handler.php');
  23. /**
  24. * FluentDOMCore implements the core and interface functions for FluentDOM
  25. *
  26. * @property string $contentType Output type - text/xml or text/html
  27. * @property-read integer $length The amount of elements found by selector.
  28. * @property-read DOMDocument $document Internal DOMDocument object
  29. * @property-read DOMXPath $xpath Internal XPath object
  30. *
  31. * @package FluentDOM
  32. */
  33. class FluentDOMCore implements IteratorAggregate, Countable, ArrayAccess {
  34. /**
  35. * Associated DOMDocument object.
  36. * @var DOMDocument $_document
  37. */
  38. protected $_document = NULL;
  39. /**
  40. * XPath object used to execute selectors
  41. * @var DOMXPath $_xpath
  42. */
  43. protected $_xpath = NULL;
  44. /**
  45. * List of namespaces to be registered for xpath expressions
  46. * @var array
  47. */
  48. protected $_namespaces = array();
  49. /**
  50. * Use document context for expression (not selected nodes).
  51. * @var boolean $_useDocumentContext
  52. */
  53. protected $_useDocumentContext = TRUE;
  54. /**
  55. * Content type for output (xml, text/xml, html, text/html).
  56. * @var string $_contentType
  57. */
  58. protected $_contentType = 'text/xml';
  59. /**
  60. * Parent FluentDOM object (previous selection in chain).
  61. * @var FluentDOM $_parent
  62. */
  63. protected $_parent = NULL;
  64. /**
  65. * Seleted element and text nodes
  66. * @var array $_array
  67. */
  68. protected $_array = array();
  69. /**
  70. * Document loader list.
  71. *
  72. * @see _initLoaders
  73. * @see _setLoader
  74. *
  75. * @var array $_loaders
  76. */
  77. protected $_loaders = NULL;
  78. /**
  79. * PHP 5.3.3 introduces a new parameter to DOMXPath::evaluate that allows to disable the
  80. * automatic namespace registration of the context node. This increases performance but
  81. * more important avoids conflicts.
  82. *
  83. * @var boolean
  84. */
  85. protected $_registerNodeNS = NULL;
  86. /**
  87. * Constructor
  88. *
  89. * @return FluentDOM
  90. */
  91. public function __construct() {
  92. $this->_document = new DOMDocument();
  93. }
  94. /**
  95. * Load a $source. The type of the source depends on the loaders. If no explicit loaders are set
  96. * FluentDOM will use a set of default loaders for xml/html and DOM.
  97. *
  98. * @param mixed $source
  99. * @param string $contentType optional, default value 'text/xml'
  100. */
  101. public function load($source, $contentType = 'text/xml') {
  102. $this->_array = array();
  103. if ($source instanceof FluentDOMCore) {
  104. $this->_useDocumentContext = FALSE;
  105. $this->_document = $source->document;
  106. $this->_xpath = $source->_xpath;
  107. $this->_contentType = $source->_contentType;
  108. $this->_parent = $source;
  109. return $this;
  110. } elseif (empty($source)) {
  111. throw new InvalidArgumentException(
  112. 'Can not load empty $source into FluentDOM object.'
  113. );
  114. } else {
  115. $this->_parent = NULL;
  116. $this->_initLoaders();
  117. foreach ($this->_loaders as $loader) {
  118. if ($loaded = $loader->load($source, $contentType)) {
  119. if ($loaded instanceof DOMDocument) {
  120. $this->_useDocumentContext = TRUE;
  121. $this->_document = $loaded;
  122. } elseif ($loaded instanceof DOMNode) {
  123. $this->_document = $loaded->ownerDocument;
  124. $this->push($loaded);
  125. $this->_useDocumentContext = FALSE;
  126. }
  127. $this->_setContentType($contentType);
  128. return $this;
  129. }
  130. }
  131. throw new InvalidArgumentException(
  132. 'Could not load invalid $source into FluentDOM object.'
  133. );
  134. }
  135. return $this;
  136. }
  137. /**
  138. * Initialize default loaders if they are not already initialized
  139. *
  140. * @return void
  141. */
  142. protected function _initLoaders() {
  143. if (!is_array($this->_loaders)) {
  144. $path = dirname(__FILE__).'/';
  145. include_once($path.'/Loader/DOMNode.php');
  146. include_once($path.'/Loader/DOMDocument.php');
  147. include_once($path.'/Loader/StringXML.php');
  148. include_once($path.'/Loader/FileXML.php');
  149. include_once($path.'/Loader/StringHTML.php');
  150. include_once($path.'/Loader/FileHTML.php');
  151. $this->_loaders = array(
  152. new FluentDOMLoaderDOMNode(),
  153. new FluentDOMLoaderDOMDocument(),
  154. new FluentDOMLoaderStringXML(),
  155. new FluentDOMLoaderFileXML(),
  156. new FluentDOMLoaderStringHTML(),
  157. new FluentDOMLoaderFileHTML(),
  158. );
  159. }
  160. }
  161. /**
  162. * Define own loading handlers
  163. *
  164. * @example iniloader/iniToXML.php Usage Example: Own loader object
  165. * @param $loaders
  166. * @return FluentDOM
  167. */
  168. public function setLoaders($loaders) {
  169. foreach ($loaders as $loader) {
  170. if (!($loader instanceof FluentDOMLoader)) {
  171. throw new InvalidArgumentException('Array contains invalid loader object');
  172. }
  173. }
  174. $this->_loaders = $loaders;
  175. return $this;
  176. }
  177. /**
  178. * Setter for FluentDOM::_contentType property
  179. *
  180. * @param string $value
  181. * @return void
  182. */
  183. protected function _setContentType($value) {
  184. switch (strtolower($value)) {
  185. case 'xml' :
  186. case 'application/xml' :
  187. case 'text/xml' :
  188. $newContentType = 'text/xml';
  189. break;
  190. case 'html' :
  191. case 'text/html' :
  192. $newContentType = 'text/html';
  193. break;
  194. default :
  195. throw new UnexpectedValueException('Invalid content type value');
  196. }
  197. if ($this->_contentType != $newContentType) {
  198. $this->_contentType = $newContentType;
  199. if (isset($this->_parent)) {
  200. $this->_parent->contentType = $newContentType;
  201. }
  202. }
  203. }
  204. /**
  205. * implement dynamic properties using magic methods
  206. *
  207. * @param string $name
  208. * @return mixed
  209. */
  210. public function __get($name) {
  211. switch ($name) {
  212. case 'contentType' :
  213. return $this->_contentType;
  214. case 'document' :
  215. return $this->_document;
  216. case 'length' :
  217. return count($this->_array);
  218. case 'xpath' :
  219. return $this->_xpath();
  220. default :
  221. return NULL;
  222. }
  223. }
  224. /**
  225. * block changes of dynamic readonly property length
  226. *
  227. * @param string $name
  228. * @param mixed $value
  229. * @return void
  230. */
  231. public function __set($name, $value) {
  232. switch ($name) {
  233. case 'contentType' :
  234. $this->_setContentType($value);
  235. break;
  236. case 'document' :
  237. case 'length' :
  238. case 'xpath' :
  239. throw new BadMethodCallException('Can not set readonly value.');
  240. default :
  241. $this->$name = $value;
  242. break;
  243. }
  244. }
  245. /**
  246. * support isset for dynamic properties length and document
  247. *
  248. * @param string $name
  249. * @return boolean
  250. */
  251. public function __isset($name) {
  252. switch ($name) {
  253. case 'length' :
  254. case 'xpath' :
  255. case 'contentType' :
  256. return TRUE;
  257. case 'document' :
  258. return isset($this->_document);
  259. }
  260. return FALSE;
  261. }
  262. /**
  263. * Return the XML output of the internal dom document
  264. *
  265. * @return string
  266. */
  267. public function __toString() {
  268. switch ($this->_contentType) {
  269. case 'html' :
  270. case 'text/html' :
  271. return $this->_document->saveHTML();
  272. default :
  273. return $this->_document->saveXML();
  274. }
  275. }
  276. /**
  277. * The item() method is used to access elements in the node list,
  278. * like in a DOMNodelist.
  279. *
  280. * @param integer $position
  281. * @return DOMNode
  282. */
  283. public function item($position) {
  284. if (isset($this->_array[$position])) {
  285. return $this->_array[$position];
  286. }
  287. return NULL;
  288. }
  289. /**
  290. * Formats the current document, resets internal node array and other properties.
  291. *
  292. * The document is saved and reloaded, all variables with DOMNodes
  293. * of this document will get invalid.
  294. *
  295. * @return FluentDOM
  296. */
  297. public function formatOutput($contentType = NULL) {
  298. if (isset($contentType)) {
  299. $this->_setContentType($contentType);
  300. }
  301. $this->_array = array();
  302. $this->_position = 0;
  303. $this->_useDocumentContext = TRUE;
  304. $this->_parent = NULL;
  305. $this->_document->preserveWhiteSpace = FALSE;
  306. $this->_document->formatOutput = TRUE;
  307. if (!empty($this->_document->documentElement)) {
  308. $this->_document->loadXML($this->_document->saveXML());
  309. }
  310. return $this;
  311. }
  312. /*
  313. * Interface - IteratorAggregate
  314. */
  315. /**
  316. * Get an iterator for this object.
  317. *
  318. * @example interfaces/Iterator.php Usage Example: Iterator Interface
  319. * @example interfaces/RecursiveIterator.php Usage Example: Recursive Iterator Interface
  320. * @return FluentDOMIterator
  321. */
  322. public function getIterator() {
  323. return new FluentDOMIterator($this);
  324. }
  325. /*
  326. * Interface - Countable
  327. */
  328. /**
  329. * Get element count (Countable interface)
  330. *
  331. * @example interfaces/Countable.php Usage Example: Countable Interface
  332. * @return integer
  333. */
  334. public function count() {
  335. return count($this->_array);
  336. }
  337. /*
  338. * Interface - ArrayAccess
  339. */
  340. /**
  341. * If somebody tries to modify the internal array throw an exception.
  342. *
  343. * @example interfaces/ArrayAccess.php Usage Example: ArrayAccess Interface
  344. * @param integer $offset
  345. * @param mixed $value
  346. * @return void
  347. */
  348. public function offsetSet($offset, $value) {
  349. throw new BadMethodCallException('List is read only');
  350. }
  351. /**
  352. * Check if index exists in internal array
  353. *
  354. * @example interfaces/ArrayAccess.php Usage Example: ArrayAccess Interface
  355. * @param integer $offset
  356. * @return boolean
  357. */
  358. public function offsetExists($offset) {
  359. return isset($this->_array[$offset]);
  360. }
  361. /**
  362. * If somebody tries to remove an element from the internal array throw an exception.
  363. *
  364. * @example interfaces/ArrayAccess.php Usage Example: ArrayAccess Interface
  365. * @param integer $offset
  366. * @return void
  367. */
  368. public function offsetUnset($offset) {
  369. throw new BadMethodCallException('List is read only');
  370. }
  371. /**
  372. * Get element from internal array
  373. *
  374. * @example interfaces/ArrayAccess.php Usage Example: ArrayAccess Interface
  375. * @param integer $offset
  376. * @return DOMNode|NULL
  377. */
  378. public function offsetGet($offset) {
  379. return isset($this->_array[$offset]) ? $this->_array[$offset] : NULL;
  380. }
  381. /*
  382. * Core functions
  383. */
  384. /**
  385. * Create a new instance of the same class with $this as the parent. This is used for the chaining.
  386. *
  387. * @param DOMNode|DOMNodeList|FluentDOM $elements
  388. * @return FluentDOMCore
  389. */
  390. public function spawn($elements = NULL) {
  391. $className = get_class($this);
  392. $result = new $className();
  393. $result->_namespaces = $this->_namespaces;
  394. $result->load($this);
  395. if (isset($elements)) {
  396. $result->push($elements);
  397. }
  398. return $result;
  399. }
  400. /**
  401. * Push new element(s) an the internal element list
  402. *
  403. * @uses _inList
  404. * @param DOMNode|DOMNodeList|FluentDOMCore $elements
  405. * @param boolean $ignoreTextNodes ignore text nodes
  406. * @return void
  407. */
  408. public function push($elements, $ignoreTextNodes = FALSE) {
  409. if ($this->_isNode($elements, $ignoreTextNodes)) {
  410. $elements = array($elements);
  411. }
  412. if ($this->_isNodeList($elements)) {
  413. foreach ($elements as $index => $node) {
  414. if ($this->_isNode($node, $ignoreTextNodes)) {
  415. if ($node->ownerDocument === $this->_document) {
  416. $this->_array[] = $node;
  417. } else {
  418. throw new OutOfBoundsException(
  419. sprintf(
  420. 'Node #%d is not a part of this document', $index
  421. )
  422. );
  423. }
  424. }
  425. }
  426. } elseif (!is_null($elements)) {
  427. throw new InvalidArgumentException('Invalid elements variable.');
  428. }
  429. }
  430. /**
  431. * Sorts an array of DOM nodes based on document position, in place, with the duplicates removed.
  432. * Note that this only works on arrays of DOM nodes, not strings or numbers.
  433. *
  434. * @param array $array array of DOM nodes
  435. * @return array
  436. */
  437. public function unique(array $array) {
  438. $sortable = array();
  439. $unsortable = array();
  440. foreach ($array as $node) {
  441. if (!($node instanceof DOMNode)) {
  442. throw new InvalidArgumentException(
  443. sprintf(
  444. 'Array must only contain dom nodes, found "%s".',
  445. is_object($node) ? get_class($node) : gettype($node)
  446. )
  447. );
  448. }
  449. if (isset($node->parentNode) ||
  450. $node === $node->ownerDocument->documentElement) {
  451. $position = (integer)$this->_xpath()->evaluate('count(preceding::node())', $node);
  452. /* use the document position as index, ignore duplicates */
  453. if (!isset($sortable[$position])) {
  454. $sortable[$position] = $node;
  455. }
  456. } else {
  457. $hash = spl_object_hash($node);
  458. /* use the object hash as index, ignore duplicates */
  459. if (!isset($unsortable[$hash])) {
  460. $unsortable[$hash] = $node;
  461. }
  462. }
  463. }
  464. ksort($sortable, SORT_NUMERIC);
  465. $result = array_values($sortable);
  466. array_splice($result, count($result), 0, array_values($unsortable));
  467. return $result;
  468. }
  469. /**
  470. * Sorts the selected nodes, with the duplicates removed.
  471. *
  472. * @uses FluentDOMCore::unique
  473. *
  474. * @param array $array array of DOM nodes
  475. * @return array
  476. */
  477. protected function _uniqueSort() {
  478. $this->_array = $this->unique($this->_array);
  479. }
  480. /**
  481. * Gives access to an xpath evaluate on the current document
  482. *
  483. * @param string $expr
  484. * @param DOMNode $context
  485. */
  486. public function evaluate($expr, DOMNode $context = NULL) {
  487. return $this->_evaluate($expr, $context);
  488. }
  489. /**
  490. * Register namespaces and or get namespaces
  491. *
  492. * @param array $namespaces If this parameter is empty the current namespaces are returned
  493. * @return array|FluentDOMCore
  494. */
  495. public function namespaces(array $namespaces = NULL) {
  496. if (is_null($namespaces)) {
  497. return $this->_namespaces;
  498. }
  499. foreach ($namespaces as $prefix => $uri) {
  500. if ($this->_isNCName($prefix)) {
  501. $this->_xpath()->registerNamespace($prefix, $uri);
  502. $this->_namespaces[$prefix] = $uri;
  503. }
  504. }
  505. return $this;
  506. }
  507. /**
  508. * Get a XPath object associated with the internal DOMDocument and register
  509. * default namespaces from the document element if availiable.
  510. *
  511. * @return DOMXPath
  512. */
  513. protected function _xpath() {
  514. if (empty($this->_xpath) || $this->_xpath->document !== $this->_document) {
  515. $this->_xpath = new DOMXPath($this->_document);
  516. foreach ($this->_namespaces as $prefix => $uri) {
  517. $this->_xpath->registerNamespace($prefix, $uri);
  518. }
  519. if ($this->_document->documentElement) {
  520. $uri = $this->_document->documentElement->lookupnamespaceURI('_');
  521. if (!isset($uri)) {
  522. $uri = $this->_document->documentElement->lookupnamespaceURI(NULL);
  523. if (isset($uri)) {
  524. $this->_xpath->registerNamespace('_', $uri);
  525. }
  526. }
  527. }
  528. }
  529. return $this->_xpath;
  530. }
  531. /**
  532. * Provides a boolean switch that eables/disables the automatic registration of namespaces
  533. * for the context node of an xpath expression evaluation.
  534. *
  535. * PHP 5.3.3 introduces a new parameter to DOMXPath::evaluate that allows to disable the
  536. * automatic namespace registration of the context node. This increases performance but
  537. * more important avoids conflicts.
  538. *
  539. * The value is initialized by checking the php version if needed.
  540. *
  541. * @param string $expr
  542. * @param DOMNode $context optional, default value NULL
  543. * @return mixed
  544. */
  545. protected function _registerNodeNamespaces($registerNodeNS = NULL) {
  546. if (isset($registerNodeNS)) {
  547. $this->_registerNodeNS = $registerNodeNS;
  548. }
  549. if (is_null($this->_registerNodeNS)) {
  550. $this->_registerNodeNS = version_compare(PHP_VERSION, '5.3.3', '<');
  551. }
  552. return $this->_registerNodeNS;
  553. }
  554. /**
  555. * Evaluate and XPath expression agains context and return the result.
  556. *
  557. * @param string $expr
  558. * @param DOMNode $context optional, default value NULL
  559. * @return mixed
  560. */
  561. protected function _evaluate($expr, DOMNode $context = NULL) {
  562. if (!$this->_registerNodeNamespaces()) {
  563. return $this->_xpath()->evaluate($expr, $context, FALSE);
  564. } elseif (isset($context)) {
  565. return $this->_xpath()->evaluate($expr, $context);
  566. } else {
  567. return $this->_xpath()->evaluate($expr);
  568. }
  569. }
  570. /**
  571. * Match XPath expression agains context and return matched elements.
  572. *
  573. * @param string $expr
  574. * @param DOMNode $context optional, default value NULL
  575. * @return DOMNodeList
  576. */
  577. protected function _match($expr, DOMNode $context = NULL) {
  578. $list = $this->_evaluate($expr, $context);
  579. if ($list instanceof DOMNodeList) {
  580. return $list;
  581. } else {
  582. throw new InvalidArgumentException('Given xpath expression did not return an node list.');
  583. }
  584. }
  585. /**
  586. * Test xpath expression against context and return true/false
  587. *
  588. * @param string $expr
  589. * @param DOMNode $context optional, default value NULL
  590. * @return boolean
  591. */
  592. protected function _test($expr, DOMNode $context = NULL) {
  593. $check = $this->_evaluate($expr, $context);
  594. if ($check instanceof DOMNodeList) {
  595. return $check->length > 0;
  596. } else {
  597. return (bool)$check;
  598. }
  599. }
  600. /**
  601. * Check if object is already in internal list
  602. *
  603. * @param DOMNode $node
  604. * @return boolean
  605. */
  606. protected function _inList($node) {
  607. foreach ($this->_array as $compareNode) {
  608. if ($compareNode === $node) {
  609. return TRUE;
  610. }
  611. }
  612. return FALSE;
  613. }
  614. /**
  615. * Validate string as qualified node name
  616. *
  617. * @param string $name
  618. * @return boolean
  619. */
  620. protected function _isQName($name) {
  621. if (empty($name)) {
  622. throw new UnexpectedValueException('Invalid QName: QName is empty.');
  623. } elseif (FALSE !== ($position = strpos($name, ':'))) {
  624. $this->_isNCName($name, 0, $position);
  625. $this->_isNCName($name, $position + 1);
  626. return TRUE;
  627. }
  628. $this->_isNCName($name);
  629. return TRUE;
  630. }
  631. /**
  632. * Validate string as qualified node name part (namespace or local name)
  633. *
  634. * @param string $name full QName
  635. * @param integer $offset Offset of NCName part in QName
  636. * @param integer $length Length of NCName part in QName
  637. * @return boolean
  638. */
  639. protected function _isNCName($name, $offset = 0, $length = 0) {
  640. $nameStartChar =
  641. 'A-Z_a-z'.
  642. '\\x{C0}-\\x{D6}\\x{D8}-\\x{F6}\\x{F8}-\\x{2FF}\\x{370}-\\x{37D}'.
  643. '\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}'.
  644. '\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}'.
  645. '\\x{FDF0}-\\x{FFFD}\\x{10000}-\\x{EFFFF}';
  646. $nameChar =
  647. $nameStartChar.
  648. '\\.\\d\\x{B7}\\x{300}-\\x{36F}\\x{203F}-\\x{2040}';
  649. if ($length > 0) {
  650. $namePart = substr($name, $offset, $length);
  651. } elseif ($offset > 0) {
  652. $namePart = substr($name, $offset);
  653. } else {
  654. $namePart = $name;
  655. }
  656. if (empty($namePart)) {
  657. throw new UnexpectedValueException(
  658. 'Invalid QName "'.$name.'": Missing QName part.'
  659. );
  660. } elseif (preg_match('([^'.$nameChar.'-])u', $namePart, $match, PREG_OFFSET_CAPTURE)) {
  661. //invalid bytes and whitespaces
  662. $position = (int)$match[0][1];
  663. throw new UnexpectedValueException(
  664. 'Invalid QName "'.$name.'": Invalid character at index '.($offset + $position).'.'
  665. );
  666. } elseif (preg_match('(^[^'.$nameStartChar.'])u', $namePart)) {
  667. //first char is a little more limited
  668. throw new UnexpectedValueException(
  669. 'Invalid QName "'.$name.'": Invalid character at index '.$offset.'.'
  670. );
  671. }
  672. return TRUE;
  673. }
  674. /**
  675. * Check if the DOMNode is DOMElement or DOMText with content
  676. *
  677. * @param DOMNode $node
  678. * @param boolean $ignoreTextNodes
  679. * @return boolean
  680. */
  681. protected function _isNode($node, $ignoreTextNodes = FALSE) {
  682. if (is_object($node)) {
  683. if ($node instanceof DOMElement) {
  684. return TRUE;
  685. } elseif ($node instanceof DOMText) {
  686. if (!$ignoreTextNodes &&
  687. !$node->isWhitespaceInElementContent()) {
  688. return TRUE;
  689. }
  690. }
  691. }
  692. return FALSE;
  693. }
  694. /**
  695. * Check if $elements is a iterateable node list
  696. *
  697. * @param DOMNodeList|DOMDocumentFragment|Iterator|IteratorAggregate|array $list
  698. * @return boolean
  699. */
  700. protected function _isNodeList($elements) {
  701. if ($elements instanceof DOMNodeList ||
  702. $elements instanceof DOMDocumentFragment ||
  703. $elements instanceof Iterator ||
  704. $elements instanceof IteratorAggregate ||
  705. is_array($elements)) {
  706. return TRUE;
  707. }
  708. return FALSE;
  709. }
  710. /**
  711. * check if parameter is a valid callback function
  712. *
  713. * @param callback $callback
  714. * @param boolean $allowGlobalFunctions
  715. * @param boolean $silent (no InvalidArgumentException)
  716. * @return boolean
  717. */
  718. protected function _isCallback($callback, $allowGlobalFunctions, $silent) {
  719. if ($callback instanceof Closure) {
  720. return TRUE;
  721. } elseif (is_string($callback) &&
  722. $allowGlobalFunctions &&
  723. function_exists($callback)) {
  724. return is_callable($callback);
  725. } elseif (is_array($callback) &&
  726. count($callback) == 2 &&
  727. (is_object($callback[0]) || is_string($callback[0])) &&
  728. is_string($callback[1])) {
  729. return is_callable($callback);
  730. } elseif ($silent) {
  731. return FALSE;
  732. } else {
  733. throw new InvalidArgumentException('Invalid callback argument');
  734. }
  735. }
  736. /**
  737. * Convert a given content xml string into and array of nodes
  738. *
  739. * @param string $content
  740. * @param boolean $includeTextNodes
  741. * @param integer $limit
  742. * @return array
  743. */
  744. protected function _getContentFragment($content, $includeTextNodes = TRUE, $limit = 0) {
  745. $result = array();
  746. $fragment = $this->_document->createDocumentFragment();
  747. if ($fragment->appendXML($content)) {
  748. for ($i = $fragment->childNodes->length - 1; $i >= 0; $i--) {
  749. $element = $fragment->childNodes->item($i);
  750. if ($element instanceof DOMElement ||
  751. ($includeTextNodes && $this->_isNode($element))) {
  752. array_unshift($result, $element);
  753. $element->parentNode->removeChild($element);
  754. }
  755. }
  756. if ($limit > 0 && count($result) >= $limit) {
  757. return array_slice($result, 0, $limit);
  758. }
  759. return $result;
  760. } else {
  761. throw new UnexpectedValueException('Invalid document fragment');
  762. }
  763. }
  764. /**
  765. * Convert a given content into and array of nodes
  766. *
  767. * @param string|array|DOMElement|DOMText|Iterator $content
  768. * @param boolean $includeTextNodes
  769. * @param integer $limit
  770. * @return array
  771. */
  772. protected function _getContentNodes($content, $includeTextNodes = TRUE, $limit = 0) {
  773. $result = array();
  774. if ($content instanceof DOMElement) {
  775. $result = array($content);
  776. } elseif ($includeTextNodes && $this->_isNode($content)) {
  777. $result = array($content);
  778. } elseif (is_string($content)) {
  779. $result = $this->_getContentFragment($content, $includeTextNodes, $limit);
  780. } elseif ($this->_isNodeList($content)) {
  781. foreach ($content as $element) {
  782. if ($element instanceof DOMElement ||
  783. ($includeTextNodes && $this->_isNode($element))) {
  784. $result[] = $element;
  785. if ($limit > 0 && count($result) >= $limit) {
  786. break;
  787. }
  788. }
  789. }
  790. } else {
  791. throw new InvalidArgumentException('Invalid content parameter');
  792. }
  793. if (empty($result)) {
  794. throw new UnexpectedValueException('No element found');
  795. } else {
  796. //if a node is not in the current document import it
  797. foreach ($result as $index => $node) {
  798. if ($node->ownerDocument !== $this->_document) {
  799. $result[$index] = $this->_document->importNode($node, TRUE);
  800. }
  801. }
  802. }
  803. return $result;
  804. }
  805. /**
  806. * Convert $content to a DOMElement. If $content contains several elements use the first.
  807. *
  808. * @param string|array|DOMElement|DOMNodeList|Iterator $content
  809. * @return DOMElement
  810. */
  811. protected function _getContentElement($content) {
  812. if ($content instanceof DOMElement) {
  813. return $content;
  814. } else {
  815. $contentNodes = $this->_getContentNodes($content, FALSE, 1);
  816. return $contentNodes[0];
  817. }
  818. }
  819. /**
  820. * Get the target nodes from a given $selector.
  821. *
  822. * A string will be used as XPath expression.
  823. *
  824. * @param string|array|DOMNode|DOMNodeList|Iterator $selector
  825. * @return array
  826. */
  827. protected function _getTargetNodes($selector) {
  828. if ($this->_isNode($selector)) {
  829. return array($selector);
  830. } elseif (is_string($selector)) {
  831. return $this->_match($selector);
  832. } elseif ($this->_isNodeList($selector)) {
  833. return $selector;
  834. } else {
  835. throw new InvalidArgumentException('Invalid selector');
  836. }
  837. }
  838. /*
  839. * the context is the target of a selector or the current selection
  840. *
  841. * @param string|array|DOMNode|DOMNodeList|Iterator $selector
  842. * @return unknown_type
  843. */
  844. protected function _getContextNodes($selector) {
  845. if (is_null($selector)) {
  846. return $this->_array;
  847. } else {
  848. return $this->_getTargetNodes($selector);
  849. }
  850. }
  851. /**
  852. * Get the inner xml of a given node or in other words the xml of all children.
  853. * @param DOMElement $node
  854. * @return string
  855. */
  856. protected function _getInnerXml($node) {
  857. $result = '';
  858. if ($node instanceof DOMElement) {
  859. foreach ($node->childNodes as $childNode) {
  860. if ($this->_isNode($childNode)) {
  861. $result .= $this->_document->saveXML($childNode);
  862. }
  863. }
  864. } elseif ($node instanceof DOMText) {
  865. return $node->textContent;
  866. }
  867. return $result;
  868. }
  869. /**
  870. * Remove nodes from document tree
  871. *
  872. * @param string|array|DOMNode|DOMNodeList|Iterator $selector
  873. * @return array $result removed nodes
  874. */
  875. protected function _removeNodes($selector) {
  876. $targetNodes = $this->_getTargetNodes($selector);
  877. $result = array();
  878. foreach ($targetNodes as $node) {
  879. if ($node instanceof DOMNode &&
  880. isset($node->parentNode)) {
  881. $result[] = $node->parentNode->removeChild($node);
  882. }
  883. }
  884. return $result;
  885. }
  886. /**
  887. * Get the class/object providing the handler functions
  888. *
  889. * @return string|object
  890. */
  891. protected function _getHandler() {
  892. return 'FluentDOMHandler';
  893. }
  894. /**
  895. * Use a handler callback to apply a content argument to each node $targetNodes. The content
  896. * argument can be an easy setter function
  897. *
  898. * @param array|DOMNodeList $targetNodes
  899. * @param string|array|DOMElement|DOMText|DOMNodeList|Iterator|callback|Closure $content
  900. * @param callback|Closure $handler
  901. */
  902. protected function _applyContentToNodes($targetNodes, $content, $handler) {
  903. $result = array();
  904. $isEasySetterFunction = $this->_isCallback($content, FALSE, TRUE);
  905. if (!$isEasySetterFunction) {
  906. $contentNodes = $this->_getContentNodes($content);
  907. }
  908. foreach ($targetNodes as $index => $node) {
  909. if ($isEasySetterFunction) {
  910. $contentNodes = $this->_executeEasySetter(
  911. $content, $node, $index, $this->_getInnerXml($node)
  912. );
  913. }
  914. if (!empty($contentNodes)) {
  915. $resultNodes = call_user_func($handler, $node, $contentNodes);
  916. if (is_array($resultNodes)) {
  917. $result = array_merge($result, $resultNodes);
  918. }
  919. }
  920. }
  921. return $result;
  922. }
  923. /**
  924. * Execute the easy setter function for a node and return the new elements
  925. *
  926. * @param callback|Closure $easySetter
  927. * @param DOMNode $node
  928. * @param integer $index
  929. * @param string $value
  930. * @return array
  931. */
  932. protected function _executeEasySetter($easySetter, $node, $index, $value) {
  933. $contentData = call_user_func($easySetter, $node, $index, $value);
  934. if (!empty($contentData)) {
  935. return $this->_getContentNodes($contentData);
  936. }
  937. return array();
  938. }
  939. }