ClassMethods.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. <?php
  2. /**
  3. * Zend Framework (http://framework.zend.com/)
  4. *
  5. * @link http://github.com/zendframework/zf2 for the canonical source repository
  6. * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
  7. * @license http://framework.zend.com/license/new-bsd New BSD License
  8. */
  9. namespace Zend\Hydrator;
  10. use Traversable;
  11. use Zend\Stdlib\ArrayUtils;
  12. class ClassMethods extends AbstractHydrator implements HydratorOptionsInterface
  13. {
  14. /**
  15. * Holds the names of the methods used for hydration, indexed by class::property name,
  16. * false if the hydration method is not callable/usable for hydration purposes
  17. *
  18. * @var string[]|bool[]
  19. */
  20. private $hydrationMethodsCache = [];
  21. /**
  22. * A map of extraction methods to property name to be used during extraction, indexed
  23. * by class name and method name
  24. *
  25. * @var string[][]
  26. */
  27. private $extractionMethodsCache = [];
  28. /**
  29. * Flag defining whether array keys are underscore-separated (true) or camel case (false)
  30. *
  31. * @var bool
  32. */
  33. protected $underscoreSeparatedKeys = true;
  34. /**
  35. * @var Filter\FilterInterface
  36. */
  37. private $callableMethodFilter;
  38. /**
  39. * Define if extract values will use camel case or name with underscore
  40. * @param bool|array $underscoreSeparatedKeys
  41. */
  42. public function __construct($underscoreSeparatedKeys = true)
  43. {
  44. parent::__construct();
  45. $this->setUnderscoreSeparatedKeys($underscoreSeparatedKeys);
  46. $this->callableMethodFilter = new Filter\OptionalParametersFilter();
  47. $this->filterComposite->addFilter('is', new Filter\IsFilter());
  48. $this->filterComposite->addFilter('has', new Filter\HasFilter());
  49. $this->filterComposite->addFilter('get', new Filter\GetFilter());
  50. $this->filterComposite->addFilter(
  51. 'parameter',
  52. new Filter\OptionalParametersFilter(),
  53. Filter\FilterComposite::CONDITION_AND
  54. );
  55. }
  56. /**
  57. * @param array|Traversable $options
  58. * @return ClassMethods
  59. * @throws Exception\InvalidArgumentException
  60. */
  61. public function setOptions($options)
  62. {
  63. if ($options instanceof Traversable) {
  64. $options = ArrayUtils::iteratorToArray($options);
  65. } elseif (!is_array($options)) {
  66. throw new Exception\InvalidArgumentException(
  67. 'The options parameter must be an array or a Traversable'
  68. );
  69. }
  70. if (isset($options['underscoreSeparatedKeys'])) {
  71. $this->setUnderscoreSeparatedKeys($options['underscoreSeparatedKeys']);
  72. }
  73. return $this;
  74. }
  75. /**
  76. * @param bool $underscoreSeparatedKeys
  77. * @return ClassMethods
  78. */
  79. public function setUnderscoreSeparatedKeys($underscoreSeparatedKeys)
  80. {
  81. $this->underscoreSeparatedKeys = (bool) $underscoreSeparatedKeys;
  82. if ($this->underscoreSeparatedKeys) {
  83. $this->setNamingStrategy(new NamingStrategy\UnderscoreNamingStrategy);
  84. } elseif ($this->getNamingStrategy() instanceof NamingStrategy\UnderscoreNamingStrategy) {
  85. $this->removeNamingStrategy();
  86. }
  87. return $this;
  88. }
  89. /**
  90. * @return bool
  91. */
  92. public function getUnderscoreSeparatedKeys()
  93. {
  94. return $this->underscoreSeparatedKeys;
  95. }
  96. /**
  97. * Extract values from an object with class methods
  98. *
  99. * Extracts the getter/setter of the given $object.
  100. *
  101. * @param object $object
  102. * @return array
  103. * @throws Exception\BadMethodCallException for a non-object $object
  104. */
  105. public function extract($object)
  106. {
  107. if (!is_object($object)) {
  108. throw new Exception\BadMethodCallException(sprintf(
  109. '%s expects the provided $object to be a PHP object)',
  110. __METHOD__
  111. ));
  112. }
  113. $objectClass = get_class($object);
  114. // reset the hydrator's hydrator's cache for this object, as the filter may be per-instance
  115. if ($object instanceof Filter\FilterProviderInterface) {
  116. $this->extractionMethodsCache[$objectClass] = null;
  117. }
  118. // pass 1 - finding out which properties can be extracted, with which methods (populate hydration cache)
  119. if (! isset($this->extractionMethodsCache[$objectClass])) {
  120. $this->extractionMethodsCache[$objectClass] = [];
  121. $filter = $this->filterComposite;
  122. $methods = get_class_methods($object);
  123. if ($object instanceof Filter\FilterProviderInterface) {
  124. $filter = new Filter\FilterComposite(
  125. [$object->getFilter()],
  126. [new Filter\MethodMatchFilter('getFilter')]
  127. );
  128. }
  129. foreach ($methods as $method) {
  130. $methodFqn = $objectClass . '::' . $method;
  131. if (! ($filter->filter($methodFqn) && $this->callableMethodFilter->filter($methodFqn))) {
  132. continue;
  133. }
  134. $attribute = $method;
  135. if (strpos($method, 'get') === 0) {
  136. $attribute = substr($method, 3);
  137. if (!property_exists($object, $attribute)) {
  138. $attribute = lcfirst($attribute);
  139. }
  140. }
  141. $this->extractionMethodsCache[$objectClass][$method] = $attribute;
  142. }
  143. }
  144. $values = [];
  145. // pass 2 - actually extract data
  146. foreach ($this->extractionMethodsCache[$objectClass] as $methodName => $attributeName) {
  147. $realAttributeName = $this->extractName($attributeName, $object);
  148. $values[$realAttributeName] = $this->extractValue($realAttributeName, $object->$methodName(), $object);
  149. }
  150. return $values;
  151. }
  152. /**
  153. * Hydrate an object by populating getter/setter methods
  154. *
  155. * Hydrates an object by getter/setter methods of the object.
  156. *
  157. * @param array $data
  158. * @param object $object
  159. * @return object
  160. * @throws Exception\BadMethodCallException for a non-object $object
  161. */
  162. public function hydrate(array $data, $object)
  163. {
  164. if (!is_object($object)) {
  165. throw new Exception\BadMethodCallException(sprintf(
  166. '%s expects the provided $object to be a PHP object)',
  167. __METHOD__
  168. ));
  169. }
  170. $objectClass = get_class($object);
  171. foreach ($data as $property => $value) {
  172. $propertyFqn = $objectClass . '::$' . $property;
  173. if (! isset($this->hydrationMethodsCache[$propertyFqn])) {
  174. $setterName = 'set' . ucfirst($this->hydrateName($property, $data));
  175. $this->hydrationMethodsCache[$propertyFqn] = is_callable([$object, $setterName])
  176. ? $setterName
  177. : false;
  178. }
  179. if ($this->hydrationMethodsCache[$propertyFqn]) {
  180. $object->{$this->hydrationMethodsCache[$propertyFqn]}($this->hydrateValue($property, $value, $data));
  181. }
  182. }
  183. return $object;
  184. }
  185. /**
  186. * {@inheritDoc}
  187. */
  188. public function addFilter($name, $filter, $condition = Filter\FilterComposite::CONDITION_OR)
  189. {
  190. $this->resetCaches();
  191. return parent::addFilter($name, $filter, $condition);
  192. }
  193. /**
  194. * {@inheritDoc}
  195. */
  196. public function removeFilter($name)
  197. {
  198. $this->resetCaches();
  199. return parent::removeFilter($name);
  200. }
  201. /**
  202. * {@inheritDoc}
  203. */
  204. public function setNamingStrategy(NamingStrategy\NamingStrategyInterface $strategy)
  205. {
  206. $this->resetCaches();
  207. return parent::setNamingStrategy($strategy);
  208. }
  209. /**
  210. * {@inheritDoc}
  211. */
  212. public function removeNamingStrategy()
  213. {
  214. $this->resetCaches();
  215. return parent::removeNamingStrategy();
  216. }
  217. /**
  218. * Reset all local hydration/extraction caches
  219. */
  220. private function resetCaches()
  221. {
  222. $this->hydrationMethodsCache = $this->extractionMethodsCache = [];
  223. }
  224. }