PropertyAccessor.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\PropertyAccess;
  11. use Symfony\Component\PropertyAccess\Exception\AccessException;
  12. use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
  13. use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
  14. use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
  15. use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
  16. /**
  17. * Default implementation of {@link PropertyAccessorInterface}.
  18. *
  19. * @author Bernhard Schussek <bschussek@gmail.com>
  20. * @author Kévin Dunglas <dunglas@gmail.com>
  21. * @author Nicolas Grekas <p@tchwork.com>
  22. */
  23. class PropertyAccessor implements PropertyAccessorInterface
  24. {
  25. /**
  26. * @internal
  27. */
  28. const VALUE = 0;
  29. /**
  30. * @internal
  31. */
  32. const REF = 1;
  33. /**
  34. * @internal
  35. */
  36. const IS_REF_CHAINED = 2;
  37. /**
  38. * @internal
  39. */
  40. const ACCESS_HAS_PROPERTY = 0;
  41. /**
  42. * @internal
  43. */
  44. const ACCESS_TYPE = 1;
  45. /**
  46. * @internal
  47. */
  48. const ACCESS_NAME = 2;
  49. /**
  50. * @internal
  51. */
  52. const ACCESS_REF = 3;
  53. /**
  54. * @internal
  55. */
  56. const ACCESS_ADDER = 4;
  57. /**
  58. * @internal
  59. */
  60. const ACCESS_REMOVER = 5;
  61. /**
  62. * @internal
  63. */
  64. const ACCESS_TYPE_METHOD = 0;
  65. /**
  66. * @internal
  67. */
  68. const ACCESS_TYPE_PROPERTY = 1;
  69. /**
  70. * @internal
  71. */
  72. const ACCESS_TYPE_MAGIC = 2;
  73. /**
  74. * @internal
  75. */
  76. const ACCESS_TYPE_ADDER_AND_REMOVER = 3;
  77. /**
  78. * @internal
  79. */
  80. const ACCESS_TYPE_NOT_FOUND = 4;
  81. /**
  82. * @var bool
  83. */
  84. private $magicCall;
  85. /**
  86. * @var bool
  87. */
  88. private $ignoreInvalidIndices;
  89. /**
  90. * @var array
  91. */
  92. private $readPropertyCache = array();
  93. /**
  94. * @var array
  95. */
  96. private $writePropertyCache = array();
  97. private static $previousErrorHandler = false;
  98. private static $errorHandler = array(__CLASS__, 'handleError');
  99. private static $resultProto = array(self::VALUE => null);
  100. /**
  101. * Should not be used by application code. Use
  102. * {@link PropertyAccess::createPropertyAccessor()} instead.
  103. *
  104. * @param bool $magicCall
  105. * @param bool $throwExceptionOnInvalidIndex
  106. */
  107. public function __construct($magicCall = false, $throwExceptionOnInvalidIndex = false)
  108. {
  109. $this->magicCall = $magicCall;
  110. $this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex;
  111. }
  112. /**
  113. * {@inheritdoc}
  114. */
  115. public function getValue($objectOrArray, $propertyPath)
  116. {
  117. if (!$propertyPath instanceof PropertyPathInterface) {
  118. $propertyPath = new PropertyPath($propertyPath);
  119. }
  120. $zval = array(
  121. self::VALUE => $objectOrArray,
  122. );
  123. $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
  124. return $propertyValues[count($propertyValues) - 1][self::VALUE];
  125. }
  126. /**
  127. * {@inheritdoc}
  128. */
  129. public function setValue(&$objectOrArray, $propertyPath, $value)
  130. {
  131. if (!$propertyPath instanceof PropertyPathInterface) {
  132. $propertyPath = new PropertyPath($propertyPath);
  133. }
  134. $zval = array(
  135. self::VALUE => $objectOrArray,
  136. self::REF => &$objectOrArray,
  137. );
  138. $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1);
  139. $overwrite = true;
  140. try {
  141. if (PHP_VERSION_ID < 70000 && false === self::$previousErrorHandler) {
  142. self::$previousErrorHandler = set_error_handler(self::$errorHandler);
  143. }
  144. for ($i = count($propertyValues) - 1; 0 <= $i; --$i) {
  145. $zval = $propertyValues[$i];
  146. unset($propertyValues[$i]);
  147. // You only need set value for current element if:
  148. // 1. it's the parent of the last index element
  149. // OR
  150. // 2. its child is not passed by reference
  151. //
  152. // This may avoid uncessary value setting process for array elements.
  153. // For example:
  154. // '[a][b][c]' => 'old-value'
  155. // If you want to change its value to 'new-value',
  156. // you only need set value for '[a][b][c]' and it's safe to ignore '[a][b]' and '[a]'
  157. //
  158. if ($overwrite) {
  159. $property = $propertyPath->getElement($i);
  160. if ($propertyPath->isIndex($i)) {
  161. if ($overwrite = !isset($zval[self::REF])) {
  162. $ref = &$zval[self::REF];
  163. $ref = $zval[self::VALUE];
  164. }
  165. $this->writeIndex($zval, $property, $value);
  166. if ($overwrite) {
  167. $zval[self::VALUE] = $zval[self::REF];
  168. }
  169. } else {
  170. $this->writeProperty($zval, $property, $value);
  171. }
  172. // if current element is an object
  173. // OR
  174. // if current element's reference chain is not broken - current element
  175. // as well as all its ancients in the property path are all passed by reference,
  176. // then there is no need to continue the value setting process
  177. if (is_object($zval[self::VALUE]) || isset($zval[self::IS_REF_CHAINED])) {
  178. break;
  179. }
  180. }
  181. $value = $zval[self::VALUE];
  182. }
  183. } catch (\TypeError $e) {
  184. try {
  185. self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0);
  186. } catch (InvalidArgumentException $e) {
  187. }
  188. } catch (\Exception $e) {
  189. } catch (\Throwable $e) {
  190. }
  191. if (PHP_VERSION_ID < 70000 && false !== self::$previousErrorHandler) {
  192. restore_error_handler();
  193. self::$previousErrorHandler = false;
  194. }
  195. if (isset($e)) {
  196. throw $e;
  197. }
  198. }
  199. /**
  200. * @internal
  201. */
  202. public static function handleError($type, $message, $file, $line, $context)
  203. {
  204. if (E_RECOVERABLE_ERROR === $type) {
  205. self::throwInvalidArgumentException($message, debug_backtrace(false), 1);
  206. }
  207. return null !== self::$previousErrorHandler && false !== call_user_func(self::$previousErrorHandler, $type, $message, $file, $line, $context);
  208. }
  209. private static function throwInvalidArgumentException($message, $trace, $i)
  210. {
  211. if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file']) {
  212. $pos = strpos($message, $delim = 'must be of the type ') ?: strpos($message, $delim = 'must be an instance of ');
  213. $pos += strlen($delim);
  214. $type = $trace[$i]['args'][0];
  215. $type = is_object($type) ? get_class($type) : gettype($type);
  216. throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given', substr($message, $pos, strpos($message, ',', $pos) - $pos), $type));
  217. }
  218. }
  219. /**
  220. * {@inheritdoc}
  221. */
  222. public function isReadable($objectOrArray, $propertyPath)
  223. {
  224. if (!$propertyPath instanceof PropertyPathInterface) {
  225. $propertyPath = new PropertyPath($propertyPath);
  226. }
  227. try {
  228. $zval = array(
  229. self::VALUE => $objectOrArray,
  230. );
  231. $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
  232. return true;
  233. } catch (AccessException $e) {
  234. return false;
  235. } catch (UnexpectedTypeException $e) {
  236. return false;
  237. }
  238. }
  239. /**
  240. * {@inheritdoc}
  241. */
  242. public function isWritable($objectOrArray, $propertyPath)
  243. {
  244. if (!$propertyPath instanceof PropertyPathInterface) {
  245. $propertyPath = new PropertyPath($propertyPath);
  246. }
  247. try {
  248. $zval = array(
  249. self::VALUE => $objectOrArray,
  250. );
  251. $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1);
  252. for ($i = count($propertyValues) - 1; 0 <= $i; --$i) {
  253. $zval = $propertyValues[$i];
  254. unset($propertyValues[$i]);
  255. if ($propertyPath->isIndex($i)) {
  256. if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) {
  257. return false;
  258. }
  259. } else {
  260. if (!$this->isPropertyWritable($zval[self::VALUE], $propertyPath->getElement($i))) {
  261. return false;
  262. }
  263. }
  264. if (is_object($zval[self::VALUE])) {
  265. return true;
  266. }
  267. }
  268. return true;
  269. } catch (AccessException $e) {
  270. return false;
  271. } catch (UnexpectedTypeException $e) {
  272. return false;
  273. }
  274. }
  275. /**
  276. * Reads the path from an object up to a given path index.
  277. *
  278. * @param array $zval The array containing the object or array to read from
  279. * @param PropertyPathInterface $propertyPath The property path to read
  280. * @param int $lastIndex The index up to which should be read
  281. * @param bool $ignoreInvalidIndices Whether to ignore invalid indices or throw an exception
  282. *
  283. * @return array The values read in the path
  284. *
  285. * @throws UnexpectedTypeException If a value within the path is neither object nor array.
  286. * @throws NoSuchIndexException If a non-existing index is accessed
  287. */
  288. private function readPropertiesUntil($zval, PropertyPathInterface $propertyPath, $lastIndex, $ignoreInvalidIndices = true)
  289. {
  290. if (!is_object($zval[self::VALUE]) && !is_array($zval[self::VALUE])) {
  291. throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, 0);
  292. }
  293. // Add the root object to the list
  294. $propertyValues = array($zval);
  295. for ($i = 0; $i < $lastIndex; ++$i) {
  296. $property = $propertyPath->getElement($i);
  297. $isIndex = $propertyPath->isIndex($i);
  298. if ($isIndex) {
  299. // Create missing nested arrays on demand
  300. if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) ||
  301. (is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !array_key_exists($property, $zval[self::VALUE]))
  302. ) {
  303. if (!$ignoreInvalidIndices) {
  304. if (!is_array($zval[self::VALUE])) {
  305. if (!$zval[self::VALUE] instanceof \Traversable) {
  306. throw new NoSuchIndexException(sprintf(
  307. 'Cannot read index "%s" while trying to traverse path "%s".',
  308. $property,
  309. (string) $propertyPath
  310. ));
  311. }
  312. $zval[self::VALUE] = iterator_to_array($zval[self::VALUE]);
  313. }
  314. throw new NoSuchIndexException(sprintf(
  315. 'Cannot read index "%s" while trying to traverse path "%s". Available indices are "%s".',
  316. $property,
  317. (string) $propertyPath,
  318. print_r(array_keys($zval[self::VALUE]), true)
  319. ));
  320. }
  321. if ($i + 1 < $propertyPath->getLength()) {
  322. if (isset($zval[self::REF])) {
  323. $zval[self::VALUE][$property] = array();
  324. $zval[self::REF] = $zval[self::VALUE];
  325. } else {
  326. $zval[self::VALUE] = array($property => array());
  327. }
  328. }
  329. }
  330. $zval = $this->readIndex($zval, $property);
  331. } else {
  332. $zval = $this->readProperty($zval, $property);
  333. }
  334. // the final value of the path must not be validated
  335. if ($i + 1 < $propertyPath->getLength() && !is_object($zval[self::VALUE]) && !is_array($zval[self::VALUE])) {
  336. throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, $i + 1);
  337. }
  338. if (isset($zval[self::REF]) && (0 === $i || isset($propertyValues[$i - 1][self::IS_REF_CHAINED]))) {
  339. // Set the IS_REF_CHAINED flag to true if:
  340. // current property is passed by reference and
  341. // it is the first element in the property path or
  342. // the IS_REF_CHAINED flag of its parent element is true
  343. // Basically, this flag is true only when the reference chain from the top element to current element is not broken
  344. $zval[self::IS_REF_CHAINED] = true;
  345. }
  346. $propertyValues[] = $zval;
  347. }
  348. return $propertyValues;
  349. }
  350. /**
  351. * Reads a key from an array-like structure.
  352. *
  353. * @param array $zval The array containing the array or \ArrayAccess object to read from
  354. * @param string|int $index The key to read
  355. *
  356. * @return array The array containing the value of the key
  357. *
  358. * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
  359. */
  360. private function readIndex($zval, $index)
  361. {
  362. if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) {
  363. throw new NoSuchIndexException(sprintf('Cannot read index "%s" from object of type "%s" because it doesn\'t implement \ArrayAccess.', $index, get_class($zval[self::VALUE])));
  364. }
  365. $result = self::$resultProto;
  366. if (isset($zval[self::VALUE][$index])) {
  367. $result[self::VALUE] = $zval[self::VALUE][$index];
  368. if (!isset($zval[self::REF])) {
  369. // Save creating references when doing read-only lookups
  370. } elseif (is_array($zval[self::VALUE])) {
  371. $result[self::REF] = &$zval[self::REF][$index];
  372. } elseif (is_object($result[self::VALUE])) {
  373. $result[self::REF] = $result[self::VALUE];
  374. }
  375. }
  376. return $result;
  377. }
  378. /**
  379. * Reads the a property from an object.
  380. *
  381. * @param array $zval The array containing the object to read from
  382. * @param string $property The property to read
  383. *
  384. * @return array The array containing the value of the property
  385. *
  386. * @throws NoSuchPropertyException If the property does not exist or is not public.
  387. */
  388. private function readProperty($zval, $property)
  389. {
  390. if (!is_object($zval[self::VALUE])) {
  391. throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%s]" instead.', $property, $property));
  392. }
  393. $result = self::$resultProto;
  394. $object = $zval[self::VALUE];
  395. $access = $this->getReadAccessInfo(get_class($object), $property);
  396. if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
  397. $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
  398. } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
  399. $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
  400. if ($access[self::ACCESS_REF] && isset($zval[self::REF])) {
  401. $result[self::REF] = &$object->{$access[self::ACCESS_NAME]};
  402. }
  403. } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
  404. // Needed to support \stdClass instances. We need to explicitly
  405. // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
  406. // a *protected* property was found on the class, property_exists()
  407. // returns true, consequently the following line will result in a
  408. // fatal error.
  409. $result[self::VALUE] = $object->$property;
  410. if (isset($zval[self::REF])) {
  411. $result[self::REF] = &$object->$property;
  412. }
  413. } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
  414. // we call the getter and hope the __call do the job
  415. $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
  416. } else {
  417. throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
  418. }
  419. // Objects are always passed around by reference
  420. if (isset($zval[self::REF]) && is_object($result[self::VALUE])) {
  421. $result[self::REF] = $result[self::VALUE];
  422. }
  423. return $result;
  424. }
  425. /**
  426. * Guesses how to read the property value.
  427. *
  428. * @param string $class
  429. * @param string $property
  430. *
  431. * @return array
  432. */
  433. private function getReadAccessInfo($class, $property)
  434. {
  435. $key = $class.'::'.$property;
  436. if (isset($this->readPropertyCache[$key])) {
  437. $access = $this->readPropertyCache[$key];
  438. } else {
  439. $access = array();
  440. $reflClass = new \ReflectionClass($class);
  441. $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
  442. $camelProp = $this->camelize($property);
  443. $getter = 'get'.$camelProp;
  444. $getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
  445. $isser = 'is'.$camelProp;
  446. $hasser = 'has'.$camelProp;
  447. if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
  448. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  449. $access[self::ACCESS_NAME] = $getter;
  450. } elseif ($reflClass->hasMethod($getsetter) && $reflClass->getMethod($getsetter)->isPublic()) {
  451. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  452. $access[self::ACCESS_NAME] = $getsetter;
  453. } elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) {
  454. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  455. $access[self::ACCESS_NAME] = $isser;
  456. } elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) {
  457. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  458. $access[self::ACCESS_NAME] = $hasser;
  459. } elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) {
  460. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  461. $access[self::ACCESS_NAME] = $property;
  462. $access[self::ACCESS_REF] = false;
  463. } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
  464. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  465. $access[self::ACCESS_NAME] = $property;
  466. $access[self::ACCESS_REF] = true;
  467. } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
  468. // we call the getter and hope the __call do the job
  469. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
  470. $access[self::ACCESS_NAME] = $getter;
  471. } else {
  472. $methods = array($getter, $getsetter, $isser, $hasser, '__get');
  473. if ($this->magicCall) {
  474. $methods[] = '__call';
  475. }
  476. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
  477. $access[self::ACCESS_NAME] = sprintf(
  478. 'Neither the property "%s" nor one of the methods "%s()" '.
  479. 'exist and have public access in class "%s".',
  480. $property,
  481. implode('()", "', $methods),
  482. $reflClass->name
  483. );
  484. }
  485. $this->readPropertyCache[$key] = $access;
  486. }
  487. return $access;
  488. }
  489. /**
  490. * Sets the value of an index in a given array-accessible value.
  491. *
  492. * @param array $zval The array containing the array or \ArrayAccess object to write to
  493. * @param string|int $index The index to write at
  494. * @param mixed $value The value to write
  495. *
  496. * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
  497. */
  498. private function writeIndex($zval, $index, $value)
  499. {
  500. if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) {
  501. throw new NoSuchIndexException(sprintf('Cannot modify index "%s" in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($zval[self::VALUE])));
  502. }
  503. $zval[self::REF][$index] = $value;
  504. }
  505. /**
  506. * Sets the value of a property in the given object.
  507. *
  508. * @param array $zval The array containing the object to write to
  509. * @param string $property The property to write
  510. * @param mixed $value The value to write
  511. *
  512. * @throws NoSuchPropertyException If the property does not exist or is not public.
  513. */
  514. private function writeProperty($zval, $property, $value)
  515. {
  516. if (!is_object($zval[self::VALUE])) {
  517. throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
  518. }
  519. $object = $zval[self::VALUE];
  520. $access = $this->getWriteAccessInfo(get_class($object), $property, $value);
  521. if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
  522. $object->{$access[self::ACCESS_NAME]}($value);
  523. } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
  524. $object->{$access[self::ACCESS_NAME]} = $value;
  525. } elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) {
  526. $this->writeCollection($zval, $property, $value, $access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]);
  527. } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
  528. // Needed to support \stdClass instances. We need to explicitly
  529. // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
  530. // a *protected* property was found on the class, property_exists()
  531. // returns true, consequently the following line will result in a
  532. // fatal error.
  533. $object->$property = $value;
  534. } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
  535. $object->{$access[self::ACCESS_NAME]}($value);
  536. } else {
  537. throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
  538. }
  539. }
  540. /**
  541. * Adjusts a collection-valued property by calling add*() and remove*() methods.
  542. *
  543. * @param array $zval The array containing the object to write to
  544. * @param string $property The property to write
  545. * @param array|\Traversable $collection The collection to write
  546. * @param string $addMethod The add*() method
  547. * @param string $removeMethod The remove*() method
  548. */
  549. private function writeCollection($zval, $property, $collection, $addMethod, $removeMethod)
  550. {
  551. // At this point the add and remove methods have been found
  552. $previousValue = $this->readProperty($zval, $property);
  553. $previousValue = $previousValue[self::VALUE];
  554. if ($previousValue instanceof \Traversable) {
  555. $previousValue = iterator_to_array($previousValue);
  556. }
  557. if ($previousValue && is_array($previousValue)) {
  558. if (is_object($collection)) {
  559. $collection = iterator_to_array($collection);
  560. }
  561. foreach ($previousValue as $key => $item) {
  562. if (!in_array($item, $collection, true)) {
  563. unset($previousValue[$key]);
  564. $zval[self::VALUE]->{$removeMethod}($item);
  565. }
  566. }
  567. } else {
  568. $previousValue = false;
  569. }
  570. foreach ($collection as $item) {
  571. if (!$previousValue || !in_array($item, $previousValue, true)) {
  572. $zval[self::VALUE]->{$addMethod}($item);
  573. }
  574. }
  575. }
  576. /**
  577. * Guesses how to write the property value.
  578. *
  579. * @param string $class
  580. * @param string $property
  581. * @param mixed $value
  582. *
  583. * @return array
  584. */
  585. private function getWriteAccessInfo($class, $property, $value)
  586. {
  587. $key = $class.'::'.$property;
  588. if (isset($this->writePropertyCache[$key])) {
  589. $access = $this->writePropertyCache[$key];
  590. } else {
  591. $access = array();
  592. $reflClass = new \ReflectionClass($class);
  593. $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
  594. $camelized = $this->camelize($property);
  595. $singulars = (array) StringUtil::singularify($camelized);
  596. if (is_array($value) || $value instanceof \Traversable) {
  597. $methods = $this->findAdderAndRemover($reflClass, $singulars);
  598. if (null !== $methods) {
  599. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
  600. $access[self::ACCESS_ADDER] = $methods[0];
  601. $access[self::ACCESS_REMOVER] = $methods[1];
  602. }
  603. }
  604. if (!isset($access[self::ACCESS_TYPE])) {
  605. $setter = 'set'.$camelized;
  606. $getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
  607. if ($this->isMethodAccessible($reflClass, $setter, 1)) {
  608. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  609. $access[self::ACCESS_NAME] = $setter;
  610. } elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) {
  611. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
  612. $access[self::ACCESS_NAME] = $getsetter;
  613. } elseif ($this->isMethodAccessible($reflClass, '__set', 2)) {
  614. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  615. $access[self::ACCESS_NAME] = $property;
  616. } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
  617. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
  618. $access[self::ACCESS_NAME] = $property;
  619. } elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) {
  620. // we call the getter and hope the __call do the job
  621. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
  622. $access[self::ACCESS_NAME] = $setter;
  623. } elseif (null !== $methods = $this->findAdderAndRemover($reflClass, $singulars)) {
  624. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
  625. $access[self::ACCESS_NAME] = sprintf(
  626. 'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
  627. 'the new value must be an array or an instance of \Traversable, '.
  628. '"%s" given.',
  629. $property,
  630. $reflClass->name,
  631. implode('()", "', $methods),
  632. is_object($value) ? get_class($value) : gettype($value)
  633. );
  634. } else {
  635. $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
  636. $access[self::ACCESS_NAME] = sprintf(
  637. 'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
  638. '"__set()" or "__call()" exist and have public access in class "%s".',
  639. $property,
  640. implode('', array_map(function ($singular) {
  641. return '"add'.$singular.'()"/"remove'.$singular.'()", ';
  642. }, $singulars)),
  643. $setter,
  644. $getsetter,
  645. $reflClass->name
  646. );
  647. }
  648. }
  649. $this->writePropertyCache[$key] = $access;
  650. }
  651. return $access;
  652. }
  653. /**
  654. * Returns whether a property is writable in the given object.
  655. *
  656. * @param object $object The object to write to
  657. * @param string $property The property to write
  658. *
  659. * @return bool Whether the property is writable
  660. */
  661. private function isPropertyWritable($object, $property)
  662. {
  663. if (!is_object($object)) {
  664. return false;
  665. }
  666. $access = $this->getWriteAccessInfo(get_class($object), $property, array());
  667. return self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]
  668. || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]
  669. || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]
  670. || (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property))
  671. || self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE];
  672. }
  673. /**
  674. * Camelizes a given string.
  675. *
  676. * @param string $string Some string
  677. *
  678. * @return string The camelized version of the string
  679. */
  680. private function camelize($string)
  681. {
  682. return str_replace(' ', '', ucwords(str_replace('_', ' ', $string)));
  683. }
  684. /**
  685. * Searches for add and remove methods.
  686. *
  687. * @param \ReflectionClass $reflClass The reflection class for the given object
  688. * @param array $singulars The singular form of the property name or null
  689. *
  690. * @return array|null An array containing the adder and remover when found, null otherwise
  691. */
  692. private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars)
  693. {
  694. foreach ($singulars as $singular) {
  695. $addMethod = 'add'.$singular;
  696. $removeMethod = 'remove'.$singular;
  697. $addMethodFound = $this->isMethodAccessible($reflClass, $addMethod, 1);
  698. $removeMethodFound = $this->isMethodAccessible($reflClass, $removeMethod, 1);
  699. if ($addMethodFound && $removeMethodFound) {
  700. return array($addMethod, $removeMethod);
  701. }
  702. }
  703. }
  704. /**
  705. * Returns whether a method is public and has the number of required parameters.
  706. *
  707. * @param \ReflectionClass $class The class of the method
  708. * @param string $methodName The method name
  709. * @param int $parameters The number of parameters
  710. *
  711. * @return bool Whether the method is public and has $parameters required parameters
  712. */
  713. private function isMethodAccessible(\ReflectionClass $class, $methodName, $parameters)
  714. {
  715. if ($class->hasMethod($methodName)) {
  716. $method = $class->getMethod($methodName);
  717. if ($method->isPublic()
  718. && $method->getNumberOfRequiredParameters() <= $parameters
  719. && $method->getNumberOfParameters() >= $parameters) {
  720. return true;
  721. }
  722. }
  723. return false;
  724. }
  725. }