ArgumentsReader.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Code\Reader;
  7. class ArgumentsReader
  8. {
  9. const NO_DEFAULT_VALUE = 'NO-DEFAULT';
  10. /**
  11. * @var NamespaceResolver
  12. */
  13. private $namespaceResolver;
  14. /**
  15. * @var ScalarTypesProvider
  16. */
  17. private $scalarTypesProvider;
  18. /**
  19. * @param NamespaceResolver|null $namespaceResolver
  20. * @param ScalarTypesProvider|null $scalarTypesProvider
  21. */
  22. public function __construct(
  23. NamespaceResolver $namespaceResolver = null,
  24. ScalarTypesProvider $scalarTypesProvider = null
  25. ) {
  26. $this->namespaceResolver = $namespaceResolver ?: new NamespaceResolver();
  27. $this->scalarTypesProvider = $scalarTypesProvider ?: new ScalarTypesProvider();
  28. }
  29. /**
  30. * Get class constructor
  31. *
  32. * @param \ReflectionClass $class
  33. * @param bool $groupByPosition
  34. * @param bool $inherited
  35. * @return array
  36. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  37. * @SuppressWarnings(PHPMD.NPathComplexity)
  38. */
  39. public function getConstructorArguments(\ReflectionClass $class, $groupByPosition = false, $inherited = false)
  40. {
  41. $output = [];
  42. /**
  43. * Skip native PHP types, classes without constructor
  44. */
  45. if ($class->isInterface() || !$class->getFileName() || false == $class->hasMethod(
  46. '__construct'
  47. ) || !$inherited && $class->getConstructor()->class != $class->getName()
  48. ) {
  49. return $output;
  50. }
  51. $constructor = new \Zend\Code\Reflection\MethodReflection($class->getName(), '__construct');
  52. foreach ($constructor->getParameters() as $parameter) {
  53. $name = $parameter->getName();
  54. $position = $parameter->getPosition();
  55. $index = $groupByPosition ? $position : $name;
  56. $default = null;
  57. if ($parameter->isOptional()) {
  58. if ($parameter->isDefaultValueAvailable()) {
  59. $value = $parameter->getDefaultValue();
  60. if (true == is_array($value)) {
  61. $default = $this->_varExportMin($value);
  62. } elseif (true == is_int($value)) {
  63. $default = $value;
  64. } else {
  65. $default = $parameter->getDefaultValue();
  66. }
  67. } elseif ($parameter->allowsNull()) {
  68. $default = null;
  69. }
  70. }
  71. $output[$index] = [
  72. 'name' => $name,
  73. 'position' => $position,
  74. 'type' => $this->processType($class, $parameter),
  75. 'isOptional' => $parameter->isOptional(),
  76. 'default' => $default,
  77. ];
  78. }
  79. return $output;
  80. }
  81. /**
  82. * Process argument type.
  83. *
  84. * @param \ReflectionClass $class
  85. * @param \Zend\Code\Reflection\ParameterReflection $parameter
  86. * @return string
  87. */
  88. private function processType(\ReflectionClass $class, \Zend\Code\Reflection\ParameterReflection $parameter)
  89. {
  90. if ($parameter->getClass()) {
  91. return NamespaceResolver::NS_SEPARATOR . $parameter->getClass()->getName();
  92. }
  93. $type = $parameter->detectType();
  94. if ($type === 'null') {
  95. return null;
  96. }
  97. if (strpos($type, '[]') !== false) {
  98. return 'array';
  99. }
  100. if (!in_array($type, $this->scalarTypesProvider->getTypes())) {
  101. $availableNamespaces = $this->namespaceResolver->getImportedNamespaces(file($class->getFileName()));
  102. $availableNamespaces[0] = $class->getNamespaceName();
  103. return $this->namespaceResolver->resolveNamespace($type, $availableNamespaces);
  104. }
  105. return $type;
  106. }
  107. /**
  108. * Get arguments of parent __construct call
  109. *
  110. * @param \ReflectionClass $class
  111. * @param array $classArguments
  112. * @return array|null
  113. */
  114. public function getParentCall(\ReflectionClass $class, array $classArguments)
  115. {
  116. /** Skip native PHP types */
  117. if (!$class->getFileName()) {
  118. return null;
  119. }
  120. $trimFunction = function (&$value) {
  121. $value = trim($value, PHP_EOL . ' $');
  122. };
  123. $method = $class->getMethod('__construct');
  124. $start = $method->getStartLine();
  125. $end = $method->getEndLine();
  126. $length = $end - $start;
  127. $source = file($class->getFileName());
  128. $content = implode('', array_slice($source, $start, $length));
  129. $pattern = '/parent::__construct\(([ ' .
  130. PHP_EOL .
  131. ']*[$]{1}[a-zA-Z0-9_]*,)*[ ' .
  132. PHP_EOL .
  133. ']*' .
  134. '([$]{1}[a-zA-Z0-9_]*){1}[' .
  135. PHP_EOL .
  136. ' ]*\);/';
  137. if (!preg_match($pattern, $content, $matches)) {
  138. return null;
  139. }
  140. $arguments = $matches[0];
  141. if (!trim($arguments)) {
  142. return null;
  143. }
  144. $arguments = substr(trim($arguments), 20, -2);
  145. $arguments = explode(',', $arguments);
  146. array_walk($arguments, $trimFunction);
  147. $output = [];
  148. foreach ($arguments as $argumentPosition => $argumentName) {
  149. $type = isset($classArguments[$argumentName]) ? $classArguments[$argumentName]['type'] : null;
  150. $output[$argumentPosition] = [
  151. 'name' => $argumentName,
  152. 'position' => $argumentPosition,
  153. 'type' => $type,
  154. ];
  155. }
  156. return $output;
  157. }
  158. /**
  159. * Check argument type compatibility
  160. *
  161. * @param string $requiredType
  162. * @param string $actualType
  163. * @return bool
  164. */
  165. public function isCompatibleType($requiredType, $actualType)
  166. {
  167. /** Types are compatible if type names are equal */
  168. if ($requiredType === $actualType) {
  169. return true;
  170. }
  171. /** Types are 'semi-compatible' if one of them are undefined */
  172. if ($requiredType === null || $actualType === null) {
  173. return true;
  174. }
  175. /**
  176. * Special case for scalar arguments
  177. * Array type is compatible with array or null type. Both of these types are checked above
  178. */
  179. if ($requiredType === 'array' || $actualType === 'array') {
  180. return false;
  181. }
  182. if ($requiredType === 'mixed' || $actualType === 'mixed') {
  183. return true;
  184. }
  185. return is_subclass_of($actualType, $requiredType);
  186. }
  187. /**
  188. * Export variable value
  189. *
  190. * @param mixed $var
  191. * @return mixed|string
  192. */
  193. protected function _varExportMin($var)
  194. {
  195. if (is_array($var)) {
  196. $toImplode = [];
  197. foreach ($var as $key => $value) {
  198. $toImplode[] = var_export($key, true) . ' => ' . $this->_varExportMin($value);
  199. }
  200. $code = 'array(' . implode(', ', $toImplode) . ')';
  201. return $code;
  202. } else {
  203. return var_export($var, true);
  204. }
  205. }
  206. /**
  207. * Get constructor annotations
  208. *
  209. * @param \ReflectionClass $class
  210. * @return array
  211. */
  212. public function getAnnotations(\ReflectionClass $class)
  213. {
  214. $regexp = '(@([a-z_][a-z0-9_]+)\(([^\)]+)\))i';
  215. $docBlock = $class->getConstructor()->getDocComment();
  216. $annotations = [];
  217. preg_match_all($regexp, $docBlock, $matches);
  218. foreach (array_keys($matches[0]) as $index) {
  219. $name = $matches[1][$index];
  220. $value = trim($matches[2][$index], '" ');
  221. $annotations[$name] = $value;
  222. }
  223. return $annotations;
  224. }
  225. }