ArgumentSequence.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. <?php
  2. /**
  3. * Class constructor validator. Validates arguments sequence
  4. *
  5. * Copyright © Magento, Inc. All rights reserved.
  6. * See COPYING.txt for license details.
  7. */
  8. namespace Magento\Framework\Code\Validator;
  9. use Magento\Framework\Code\ValidatorInterface;
  10. class ArgumentSequence implements ValidatorInterface
  11. {
  12. const REQUIRED = 'required';
  13. const OPTIONAL = 'optional';
  14. /**
  15. * @var \Magento\Framework\Code\Reader\ArgumentsReader
  16. */
  17. protected $_argumentsReader;
  18. /**
  19. * @var array
  20. */
  21. protected $_cache;
  22. /**
  23. * @param \Magento\Framework\Code\Reader\ArgumentsReader $argumentsReader
  24. */
  25. public function __construct(\Magento\Framework\Code\Reader\ArgumentsReader $argumentsReader = null)
  26. {
  27. $this->_argumentsReader = $argumentsReader ?: new \Magento\Framework\Code\Reader\ArgumentsReader();
  28. }
  29. /**
  30. * Validate class
  31. *
  32. * @param string $className
  33. * @return bool
  34. * @throws \Magento\Framework\Exception\ValidatorException
  35. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  36. */
  37. public function validate($className)
  38. {
  39. $class = new \ReflectionClass($className);
  40. $classArguments = $this->_argumentsReader->getConstructorArguments($class);
  41. if ($this->_isContextOnly($classArguments)) {
  42. return true;
  43. }
  44. $parent = $class->getParentClass();
  45. $parentArguments = [];
  46. if ($parent) {
  47. $parentClass = $parent->getName();
  48. if (0 !== strpos($parentClass, '\\')) {
  49. $parentClass = '\\' . $parentClass;
  50. }
  51. if (isset($this->_cache[$parentClass])) {
  52. $parentCall = $this->_argumentsReader->getParentCall($class, []);
  53. if (empty($classArguments) || $parentCall) {
  54. $parentArguments = $this->_cache[$parentClass];
  55. }
  56. }
  57. }
  58. if (empty($classArguments)) {
  59. $classArguments = $parentArguments;
  60. }
  61. $requiredSequence = $this->_buildsSequence($classArguments, $parentArguments);
  62. if (!empty($requiredSequence)) {
  63. $this->_cache[$className] = $requiredSequence;
  64. }
  65. if (false == $this->_checkArgumentSequence($classArguments, $requiredSequence)) {
  66. $classPath = str_replace('\\', '/', $class->getFileName());
  67. throw new \Magento\Framework\Exception\ValidatorException(
  68. new \Magento\Framework\Phrase(
  69. 'Incorrect argument sequence in class %1 in %2%3Required: $%4%5Actual : $%6%7',
  70. [
  71. $className,
  72. $classPath,
  73. PHP_EOL,
  74. implode(', $', array_keys($requiredSequence)),
  75. PHP_EOL,
  76. implode(', $', array_keys($classArguments)),
  77. PHP_EOL
  78. ]
  79. )
  80. );
  81. }
  82. return true;
  83. }
  84. /**
  85. * Check argument sequence
  86. *
  87. * @param array $actualSequence
  88. * @param array $requiredSequence
  89. * @return bool
  90. */
  91. protected function _checkArgumentSequence(array $actualSequence, array $requiredSequence)
  92. {
  93. $actualArgumentSequence = [];
  94. $requiredArgumentSequence = [];
  95. foreach ($actualSequence as $name => $argument) {
  96. if (false == $argument['isOptional']) {
  97. $actualArgumentSequence[$name] = $argument;
  98. } else {
  99. break;
  100. }
  101. }
  102. foreach ($requiredSequence as $name => $argument) {
  103. if (false == $argument['isOptional']) {
  104. $requiredArgumentSequence[$name] = $argument;
  105. } else {
  106. break;
  107. }
  108. }
  109. $actual = array_keys($actualArgumentSequence);
  110. $required = array_keys($requiredArgumentSequence);
  111. return $actual === $required;
  112. }
  113. /**
  114. * Build argument required sequence
  115. *
  116. * @param array $classArguments
  117. * @param array $parentArguments
  118. * @return array
  119. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  120. * @SuppressWarnings(PHPMD.NPathComplexity)
  121. */
  122. protected function _buildsSequence(array $classArguments, array $parentArguments = [])
  123. {
  124. $output = [];
  125. if (empty($classArguments)) {
  126. return $parentArguments;
  127. }
  128. $classArgumentList = $this->_sortArguments($classArguments);
  129. $parentArgumentList = $this->_sortArguments($parentArguments);
  130. $migrated = [];
  131. foreach ($parentArgumentList[self::REQUIRED] as $name => $argument) {
  132. if (!isset($classArgumentList[self::OPTIONAL][$name])) {
  133. $output[$name] = isset(
  134. $classArgumentList[self::REQUIRED][$name]
  135. ) ? $classArgumentList[self::REQUIRED][$name] : $argument;
  136. } else {
  137. $migrated[$name] = $classArgumentList[self::OPTIONAL][$name];
  138. }
  139. }
  140. foreach ($classArgumentList[self::REQUIRED] as $name => $argument) {
  141. if (!isset($output[$name])) {
  142. $output[$name] = $argument;
  143. }
  144. }
  145. /** Use parent required argument that become optional in child class */
  146. foreach ($migrated as $name => $argument) {
  147. if (!isset($output[$name])) {
  148. $output[$name] = $argument;
  149. }
  150. }
  151. foreach ($parentArgumentList[self::OPTIONAL] as $name => $argument) {
  152. if (!isset($output[$name])) {
  153. $output[$name] = isset(
  154. $classArgumentList[self::OPTIONAL][$name]
  155. ) ? $classArgumentList[self::OPTIONAL][$name] : $argument;
  156. }
  157. }
  158. foreach ($classArgumentList[self::OPTIONAL] as $name => $argument) {
  159. if (!isset($output[$name])) {
  160. $output[$name] = $argument;
  161. }
  162. }
  163. return $output;
  164. }
  165. /**
  166. * Sort arguments
  167. *
  168. * @param array $arguments
  169. * @return array
  170. */
  171. protected function _sortArguments($arguments)
  172. {
  173. $required = [];
  174. $optional = [];
  175. foreach ($arguments as $name => $argument) {
  176. if ($argument['isOptional']) {
  177. $optional[$name] = $argument;
  178. } else {
  179. $required[$name] = $argument;
  180. }
  181. }
  182. return [self::REQUIRED => $required, self::OPTIONAL => $optional];
  183. }
  184. /**
  185. * Check whether arguments list contains an only context argument
  186. *
  187. * @param array $arguments
  188. * @return bool
  189. */
  190. protected function _isContextOnly(array $arguments)
  191. {
  192. if (count($arguments) !== 1) {
  193. return false;
  194. }
  195. $argument = current($arguments);
  196. return $argument['type'] && $this->_isContextType($argument['type']);
  197. }
  198. /**
  199. * Check whether type is context object
  200. *
  201. * @param string $type
  202. * @return bool
  203. */
  204. protected function _isContextType($type)
  205. {
  206. return is_subclass_of($type, \Magento\Framework\ObjectManager\ContextInterface::class);
  207. }
  208. }