InterfaceValidator.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Interception\Code;
  7. use Magento\Framework\Exception\ValidatorException;
  8. use Magento\Framework\Phrase;
  9. class InterfaceValidator
  10. {
  11. const METHOD_BEFORE = 'before';
  12. const METHOD_AROUND = 'around';
  13. const METHOD_AFTER = 'after';
  14. /**
  15. * Arguments reader model
  16. *
  17. * @var \Magento\Framework\Code\Reader\ArgumentsReader
  18. */
  19. protected $_argumentsReader;
  20. /**
  21. * @param \Magento\Framework\Code\Reader\ArgumentsReader $argumentsReader
  22. */
  23. public function __construct(\Magento\Framework\Code\Reader\ArgumentsReader $argumentsReader = null)
  24. {
  25. $this->_argumentsReader = $argumentsReader ?: new \Magento\Framework\Code\Reader\ArgumentsReader();
  26. }
  27. /**
  28. * Validate plugin interface
  29. *
  30. * @param string $pluginClass
  31. * @param string $interceptedType
  32. *
  33. * @return void
  34. * @throws ValidatorException
  35. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  36. * @SuppressWarnings(PHPMD.UnusedLocalVariable)
  37. */
  38. public function validate($pluginClass, $interceptedType)
  39. {
  40. $interceptedType = '\\' . trim($interceptedType, '\\');
  41. $pluginClass = '\\' . trim($pluginClass, '\\');
  42. $plugin = new \ReflectionClass($pluginClass);
  43. $type = new \ReflectionClass($interceptedType);
  44. foreach ($plugin->getMethods(\ReflectionMethod::IS_PUBLIC) as $pluginMethod) {
  45. /** @var $pluginMethod \ReflectionMethod */
  46. $originMethodName = $this->getOriginMethodName($pluginMethod->getName());
  47. if ($originMethodName === null) {
  48. continue;
  49. }
  50. if (!$type->hasMethod($originMethodName)) {
  51. throw new ValidatorException(
  52. new Phrase(
  53. 'Incorrect interface in %1. There is no method [ %2 ] in %3 interface',
  54. [$pluginClass, $originMethodName, $interceptedType]
  55. )
  56. );
  57. }
  58. $originMethod = $type->getMethod($originMethodName);
  59. $pluginMethodParameters = $this->getMethodParameters($pluginMethod);
  60. $originMethodParameters = $this->getMethodParameters($originMethod);
  61. $methodType = $this->getMethodType($pluginMethod->getName());
  62. $subject = array_shift($pluginMethodParameters);
  63. if (!$this->_argumentsReader->isCompatibleType(
  64. $subject['type'],
  65. $interceptedType
  66. ) || $subject['type'] === null
  67. ) {
  68. throw new ValidatorException(
  69. new Phrase(
  70. 'Invalid [%1] $%2 type in %3::%4. It must be compatible with %5',
  71. [$subject['type'], $subject['name'], $pluginClass, $pluginMethod->getName(), $interceptedType]
  72. )
  73. );
  74. }
  75. switch ($methodType) {
  76. case self::METHOD_BEFORE:
  77. $this->validateMethodsParameters(
  78. $pluginMethodParameters,
  79. $originMethodParameters,
  80. $pluginClass,
  81. $pluginMethod->getName()
  82. );
  83. break;
  84. case self::METHOD_AROUND:
  85. $proceed = array_shift($pluginMethodParameters);
  86. if (!$this->_argumentsReader->isCompatibleType($proceed['type'], '\\Closure')) {
  87. throw new ValidatorException(
  88. new Phrase(
  89. 'Invalid [%1] $%2 type in %3::%4. It must be compatible with \\Closure',
  90. [$proceed['type'], $proceed['name'], $pluginClass, $pluginMethod->getName()]
  91. )
  92. );
  93. }
  94. $this->validateMethodsParameters(
  95. $pluginMethodParameters,
  96. $originMethodParameters,
  97. $pluginClass,
  98. $pluginMethod->getName()
  99. );
  100. break;
  101. case self::METHOD_AFTER:
  102. if (count($pluginMethodParameters) > 1) {
  103. // remove result
  104. array_shift($pluginMethodParameters);
  105. $matchedParameters = array_intersect_key($originMethodParameters, $pluginMethodParameters);
  106. $this->validateMethodsParameters(
  107. $pluginMethodParameters,
  108. $matchedParameters,
  109. $pluginClass,
  110. $pluginMethod->getName()
  111. );
  112. }
  113. break;
  114. }
  115. }
  116. }
  117. /**
  118. * Validate methods parameters compatibility
  119. *
  120. * @param array $pluginParameters
  121. * @param array $originParameters
  122. * @param string $class
  123. * @param string $method
  124. *
  125. * @return void
  126. * @throws ValidatorException
  127. */
  128. protected function validateMethodsParameters(array $pluginParameters, array $originParameters, $class, $method)
  129. {
  130. if (count($pluginParameters) != count($originParameters)) {
  131. throw new ValidatorException(
  132. new Phrase(
  133. 'Invalid method signature. Invalid method parameters count in %1::%2',
  134. [$class, $method]
  135. )
  136. );
  137. }
  138. foreach ($pluginParameters as $position => $data) {
  139. if (!$this->_argumentsReader->isCompatibleType($data['type'], $originParameters[$position]['type'])) {
  140. throw new ValidatorException(
  141. new Phrase(
  142. 'Incompatible parameter type [%1 $%2] in %3::%4. It must be compatible with %5',
  143. [$data['type'], $data['name'], $class, $method, $originParameters[$position]['type']]
  144. )
  145. );
  146. }
  147. }
  148. }
  149. /**
  150. * Get parameters type
  151. *
  152. * @param \ReflectionParameter $parameter
  153. *
  154. * @return string
  155. */
  156. protected function getParametersType(\ReflectionParameter $parameter)
  157. {
  158. $parameterClass = $parameter->getClass();
  159. $type = $parameterClass ? '\\' . $parameterClass->getName() : ($parameter->isArray() ? 'array' : null);
  160. return $type;
  161. }
  162. /**
  163. * Get intercepted method name
  164. *
  165. * @param string $pluginMethodName
  166. *
  167. * @return string|null
  168. */
  169. protected function getOriginMethodName($pluginMethodName)
  170. {
  171. switch ($this->getMethodType($pluginMethodName)) {
  172. case self::METHOD_BEFORE:
  173. case self::METHOD_AROUND:
  174. return lcfirst(substr($pluginMethodName, 6));
  175. case self::METHOD_AFTER:
  176. return lcfirst(substr($pluginMethodName, 5));
  177. default:
  178. return null;
  179. }
  180. }
  181. /**
  182. * Get method type
  183. *
  184. * @param string $pluginMethodName
  185. *
  186. * @return null|string
  187. */
  188. protected function getMethodType($pluginMethodName)
  189. {
  190. if (substr($pluginMethodName, 0, 6) == self::METHOD_BEFORE) {
  191. return self::METHOD_BEFORE;
  192. } elseif (substr($pluginMethodName, 0, 6) == self::METHOD_AROUND) {
  193. return self::METHOD_AROUND;
  194. } elseif (substr($pluginMethodName, 0, 5) == self::METHOD_AFTER) {
  195. return self::METHOD_AFTER;
  196. }
  197. return null;
  198. }
  199. /**
  200. * Get method parameters
  201. *
  202. * @param \ReflectionMethod $method
  203. *
  204. * @return array
  205. */
  206. protected function getMethodParameters(\ReflectionMethod $method)
  207. {
  208. $output = [];
  209. foreach ($method->getParameters() as $parameter) {
  210. $output[$parameter->getPosition()] = [
  211. 'name' => $parameter->getName(),
  212. 'type' => $this->getParametersType($parameter),
  213. ];
  214. }
  215. return $output;
  216. }
  217. }