PropertyAccessor.php 33 KB

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