/src/DiDom/Element.php

https://github.com/Imangazaliev/DiDOM · PHP · 401 lines · 196 code · 69 blank · 136 comment · 30 complexity · c10a08a02228abd815a95f1fb9313c22 MD5 · raw file

  1. <?php
  2. namespace DiDom;
  3. use DiDom\Exceptions\InvalidSelectorException;
  4. use DOMCdataSection;
  5. use DOMComment;
  6. use DOMDocument;
  7. use DOMElement;
  8. use DOMNode;
  9. use DOMText;
  10. use InvalidArgumentException;
  11. use LogicException;
  12. use RuntimeException;
  13. /**
  14. * @property string $tag
  15. */
  16. class Element extends Node
  17. {
  18. /**
  19. * @var ClassAttribute
  20. */
  21. protected $classAttribute;
  22. /**
  23. * @var StyleAttribute
  24. */
  25. protected $styleAttribute;
  26. /**
  27. * @param DOMElement|DOMText|DOMComment|DOMCdataSection|string $tagName The tag name of an element
  28. * @param string|null $value The value of an element
  29. * @param array $attributes The attributes of an element
  30. */
  31. public function __construct($tagName, $value = null, array $attributes = [])
  32. {
  33. if (is_string($tagName)) {
  34. $document = new DOMDocument('1.0', 'UTF-8');
  35. $node = $document->createElement($tagName);
  36. $this->setNode($node);
  37. } else {
  38. $this->setNode($tagName);
  39. }
  40. if ($value !== null) {
  41. $this->setValue($value);
  42. }
  43. foreach ($attributes as $attrName => $attrValue) {
  44. $this->setAttribute($attrName, $attrValue);
  45. }
  46. }
  47. /**
  48. * Creates a new element.
  49. *
  50. * @param DOMNode|string $name The tag name of an element
  51. * @param string|null $value The value of an element
  52. * @param array $attributes The attributes of an element
  53. *
  54. * @return Element
  55. */
  56. public static function create($name, $value = null, array $attributes = [])
  57. {
  58. return new Element($name, $value, $attributes);
  59. }
  60. /**
  61. * Creates a new element node by CSS selector.
  62. *
  63. * @param string $selector
  64. * @param string|null $value
  65. * @param array $attributes
  66. *
  67. * @return Element
  68. *
  69. * @throws InvalidSelectorException
  70. */
  71. public static function createBySelector($selector, $value = null, array $attributes = [])
  72. {
  73. return Document::create()->createElementBySelector($selector, $value, $attributes);
  74. }
  75. /**
  76. * Checks that the node matches selector.
  77. *
  78. * @param string $selector CSS selector
  79. * @param bool $strict
  80. *
  81. * @return bool
  82. *
  83. * @throws InvalidSelectorException if the selector is invalid
  84. * @throws InvalidArgumentException if the tag name is not a string
  85. * @throws RuntimeException if the tag name is not specified in strict mode
  86. */
  87. public function matches($selector, $strict = false)
  88. {
  89. if ( ! is_string($selector)) {
  90. throw new InvalidArgumentException(sprintf('%s expects parameter 1 to be string, %s given', __METHOD__, gettype($selector)));
  91. }
  92. if ( ! $this->node instanceof DOMElement) {
  93. return false;
  94. }
  95. if ($selector === '*') {
  96. return true;
  97. }
  98. if ( ! $strict) {
  99. $innerHtml = $this->html();
  100. $html = "<root>$innerHtml</root>";
  101. $selector = 'root > ' . trim($selector);
  102. $document = new Document();
  103. $document->loadHtml($html, LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED);
  104. return $document->has($selector);
  105. }
  106. $segments = Query::getSegments($selector);
  107. if ( ! array_key_exists('tag', $segments)) {
  108. throw new RuntimeException(sprintf('Tag name must be specified in %s', $selector));
  109. }
  110. if ($segments['tag'] !== $this->tag && $segments['tag'] !== '*') {
  111. return false;
  112. }
  113. $segments['id'] = array_key_exists('id', $segments) ? $segments['id'] : null;
  114. if ($segments['id'] !== $this->getAttribute('id')) {
  115. return false;
  116. }
  117. $classes = $this->hasAttribute('class') ? explode(' ', trim($this->getAttribute('class'))) : [];
  118. $segments['classes'] = array_key_exists('classes', $segments) ? $segments['classes'] : [];
  119. $diff1 = array_diff($segments['classes'], $classes);
  120. $diff2 = array_diff($classes, $segments['classes']);
  121. if (count($diff1) > 0 || count($diff2) > 0) {
  122. return false;
  123. }
  124. $attributes = $this->attributes();
  125. unset($attributes['id'], $attributes['class']);
  126. $segments['attributes'] = array_key_exists('attributes', $segments) ? $segments['attributes'] : [];
  127. $diff1 = array_diff_assoc($segments['attributes'], $attributes);
  128. $diff2 = array_diff_assoc($attributes, $segments['attributes']);
  129. // if the attributes are not equal
  130. if (count($diff1) > 0 || count($diff2) > 0) {
  131. return false;
  132. }
  133. return true;
  134. }
  135. /**
  136. * Determine if an attribute exists on the element.
  137. *
  138. * @param string $name The name of an attribute
  139. *
  140. * @return bool
  141. */
  142. public function hasAttribute($name)
  143. {
  144. return $this->node->hasAttribute($name);
  145. }
  146. /**
  147. * Set an attribute on the element.
  148. *
  149. * @param string $name The name of an attribute
  150. * @param string $value The value of an attribute
  151. *
  152. * @return Element
  153. */
  154. public function setAttribute($name, $value)
  155. {
  156. if (is_numeric($value)) {
  157. $value = (string) $value;
  158. }
  159. if ( ! is_string($value) && $value !== null) {
  160. throw new InvalidArgumentException(sprintf('%s expects parameter 2 to be string or null, %s given', __METHOD__, (is_object($value) ? get_class($value) : gettype($value))));
  161. }
  162. $this->node->setAttribute($name, $value);
  163. return $this;
  164. }
  165. /**
  166. * Access to the element's attributes.
  167. *
  168. * @param string $name The name of an attribute
  169. * @param string|null $default The value returned if the attribute doesn't exist
  170. *
  171. * @return string|null The value of an attribute or null if attribute doesn't exist
  172. */
  173. public function getAttribute($name, $default = null)
  174. {
  175. if ($this->hasAttribute($name)) {
  176. return $this->node->getAttribute($name);
  177. }
  178. return $default;
  179. }
  180. /**
  181. * Unset an attribute on the element.
  182. *
  183. * @param string $name The name of an attribute
  184. *
  185. * @return Element
  186. */
  187. public function removeAttribute($name)
  188. {
  189. $this->node->removeAttribute($name);
  190. return $this;
  191. }
  192. /**
  193. * Unset all attributes of the element.
  194. *
  195. * @param string[] $exclusions
  196. *
  197. * @return Element
  198. */
  199. public function removeAllAttributes(array $exclusions = [])
  200. {
  201. if ( ! $this->node instanceof DOMElement) {
  202. return $this;
  203. }
  204. foreach ($this->attributes() as $name => $value) {
  205. if (in_array($name, $exclusions, true)) {
  206. continue;
  207. }
  208. $this->node->removeAttribute($name);
  209. }
  210. return $this;
  211. }
  212. /**
  213. * Alias for getAttribute and setAttribute methods.
  214. *
  215. * @param string $name The name of an attribute
  216. * @param string|null $value The value that will be returned an attribute doesn't exist
  217. *
  218. * @return string|null|Element
  219. */
  220. public function attr($name, $value = null)
  221. {
  222. if ($value === null) {
  223. return $this->getAttribute($name);
  224. }
  225. return $this->setAttribute($name, $value);
  226. }
  227. /**
  228. * Returns the node attributes or null, if it is not DOMElement.
  229. *
  230. * @param string[] $names
  231. *
  232. * @return array|null
  233. */
  234. public function attributes(array $names = null)
  235. {
  236. if ( ! $this->node instanceof DOMElement) {
  237. return null;
  238. }
  239. if ($names === null) {
  240. $result = [];
  241. foreach ($this->node->attributes as $name => $attribute) {
  242. $result[$name] = $attribute->value;
  243. }
  244. return $result;
  245. }
  246. $result = [];
  247. foreach ($this->node->attributes as $name => $attribute) {
  248. if (in_array($name, $names, true)) {
  249. $result[$name] = $attribute->value;
  250. }
  251. }
  252. return $result;
  253. }
  254. /**
  255. * @return ClassAttribute
  256. *
  257. * @throws LogicException if the node is not an instance of DOMElement
  258. */
  259. public function classes()
  260. {
  261. if ($this->classAttribute !== null) {
  262. return $this->classAttribute;
  263. }
  264. if ( ! $this->isElementNode()) {
  265. throw new LogicException('Class attribute is available only for element nodes');
  266. }
  267. $this->classAttribute = new ClassAttribute($this);
  268. return $this->classAttribute;
  269. }
  270. /**
  271. * @return StyleAttribute
  272. *
  273. * @throws LogicException if the node is not an instance of DOMElement
  274. */
  275. public function style()
  276. {
  277. if ($this->styleAttribute !== null) {
  278. return $this->styleAttribute;
  279. }
  280. if ( ! $this->isElementNode()) {
  281. throw new LogicException('Style attribute is available only for element nodes');
  282. }
  283. $this->styleAttribute = new StyleAttribute($this);
  284. return $this->styleAttribute;
  285. }
  286. /**
  287. * Dynamically set an attribute on the element.
  288. *
  289. * @param string $name The name of an attribute
  290. * @param string $value The value of an attribute
  291. *
  292. * @return Element
  293. */
  294. public function __set($name, $value)
  295. {
  296. return $this->setAttribute($name, $value);
  297. }
  298. /**
  299. * Dynamically access the element's attributes.
  300. *
  301. * @param string $name The name of an attribute
  302. *
  303. * @return string|null
  304. */
  305. public function __get($name)
  306. {
  307. if ($name === 'tag') {
  308. return $this->node->tagName;
  309. }
  310. return $this->getAttribute($name);
  311. }
  312. /**
  313. * Determine if an attribute exists on the element.
  314. *
  315. * @param string $name The attribute name
  316. *
  317. * @return bool
  318. */
  319. public function __isset($name)
  320. {
  321. return $this->hasAttribute($name);
  322. }
  323. /**
  324. * Unset an attribute on the model.
  325. *
  326. * @param string $name The name of an attribute
  327. */
  328. public function __unset($name)
  329. {
  330. $this->removeAttribute($name);
  331. }
  332. }