Generator.php 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182
  1. <?php
  2. /*
  3. * This file is part of the phpunit-mock-objects package.
  4. *
  5. * (c) Sebastian Bergmann <sebastian@phpunit.de>
  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 PHPUnit\Framework\MockObject;
  11. use Doctrine\Instantiator\Exception\ExceptionInterface as InstantiatorException;
  12. use Doctrine\Instantiator\Instantiator;
  13. use Iterator;
  14. use IteratorAggregate;
  15. use PHPUnit\Framework\Exception;
  16. use PHPUnit\Util\InvalidArgumentHelper;
  17. use ReflectionClass;
  18. use ReflectionException;
  19. use ReflectionMethod;
  20. use SoapClient;
  21. use Text_Template;
  22. use Traversable;
  23. /**
  24. * Mock Object Code Generator
  25. */
  26. class Generator
  27. {
  28. /**
  29. * @var array
  30. */
  31. private static $cache = [];
  32. /**
  33. * @var Text_Template[]
  34. */
  35. private static $templates = [];
  36. /**
  37. * @var array
  38. */
  39. private $blacklistedMethodNames = [
  40. '__CLASS__' => true,
  41. '__DIR__' => true,
  42. '__FILE__' => true,
  43. '__FUNCTION__' => true,
  44. '__LINE__' => true,
  45. '__METHOD__' => true,
  46. '__NAMESPACE__' => true,
  47. '__TRAIT__' => true,
  48. '__clone' => true,
  49. '__halt_compiler' => true,
  50. ];
  51. /**
  52. * Returns a mock object for the specified class.
  53. *
  54. * @param string|string[] $type
  55. * @param array $methods
  56. * @param array $arguments
  57. * @param string $mockClassName
  58. * @param bool $callOriginalConstructor
  59. * @param bool $callOriginalClone
  60. * @param bool $callAutoload
  61. * @param bool $cloneArguments
  62. * @param bool $callOriginalMethods
  63. * @param object $proxyTarget
  64. * @param bool $allowMockingUnknownTypes
  65. *
  66. * @return MockObject
  67. *
  68. * @throws Exception
  69. * @throws RuntimeException
  70. * @throws \PHPUnit\Framework\Exception
  71. * @throws \ReflectionException
  72. */
  73. public function getMock($type, $methods = [], array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false, $proxyTarget = null, $allowMockingUnknownTypes = true)
  74. {
  75. if (!\is_array($type) && !\is_string($type)) {
  76. throw InvalidArgumentHelper::factory(1, 'array or string');
  77. }
  78. if (!\is_string($mockClassName)) {
  79. throw InvalidArgumentHelper::factory(4, 'string');
  80. }
  81. if (!\is_array($methods) && null !== $methods) {
  82. throw InvalidArgumentHelper::factory(2, 'array', $methods);
  83. }
  84. if ($type === 'Traversable' || $type === '\\Traversable') {
  85. $type = 'Iterator';
  86. }
  87. if (\is_array($type)) {
  88. $type = \array_unique(
  89. \array_map(
  90. function ($type) {
  91. if ($type === 'Traversable' ||
  92. $type === '\\Traversable' ||
  93. $type === '\\Iterator') {
  94. return 'Iterator';
  95. }
  96. return $type;
  97. },
  98. $type
  99. )
  100. );
  101. }
  102. if (!$allowMockingUnknownTypes) {
  103. if (\is_array($type)) {
  104. foreach ($type as $_type) {
  105. if (!\class_exists($_type, $callAutoload) &&
  106. !\interface_exists($_type, $callAutoload)) {
  107. throw new RuntimeException(
  108. \sprintf(
  109. 'Cannot stub or mock class or interface "%s" which does not exist',
  110. $_type
  111. )
  112. );
  113. }
  114. }
  115. } else {
  116. if (!\class_exists($type, $callAutoload) &&
  117. !\interface_exists($type, $callAutoload)
  118. ) {
  119. throw new RuntimeException(
  120. \sprintf(
  121. 'Cannot stub or mock class or interface "%s" which does not exist',
  122. $type
  123. )
  124. );
  125. }
  126. }
  127. }
  128. if (null !== $methods) {
  129. foreach ($methods as $method) {
  130. if (!\preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*~', $method)) {
  131. throw new RuntimeException(
  132. \sprintf(
  133. 'Cannot stub or mock method with invalid name "%s"',
  134. $method
  135. )
  136. );
  137. }
  138. }
  139. if ($methods !== \array_unique($methods)) {
  140. throw new RuntimeException(
  141. \sprintf(
  142. 'Cannot stub or mock using a method list that contains duplicates: "%s" (duplicate: "%s")',
  143. \implode(', ', $methods),
  144. \implode(', ', \array_unique(\array_diff_assoc($methods, \array_unique($methods))))
  145. )
  146. );
  147. }
  148. }
  149. if ($mockClassName !== '' && \class_exists($mockClassName, false)) {
  150. $reflect = new ReflectionClass($mockClassName);
  151. if (!$reflect->implementsInterface(MockObject::class)) {
  152. throw new RuntimeException(
  153. \sprintf(
  154. 'Class "%s" already exists.',
  155. $mockClassName
  156. )
  157. );
  158. }
  159. }
  160. if ($callOriginalConstructor === false && $callOriginalMethods === true) {
  161. throw new RuntimeException(
  162. 'Proxying to original methods requires invoking the original constructor'
  163. );
  164. }
  165. $mock = $this->generate(
  166. $type,
  167. $methods,
  168. $mockClassName,
  169. $callOriginalClone,
  170. $callAutoload,
  171. $cloneArguments,
  172. $callOriginalMethods
  173. );
  174. return $this->getObject(
  175. $mock['code'],
  176. $mock['mockClassName'],
  177. $type,
  178. $callOriginalConstructor,
  179. $callAutoload,
  180. $arguments,
  181. $callOriginalMethods,
  182. $proxyTarget
  183. );
  184. }
  185. /**
  186. * @param string $code
  187. * @param string $className
  188. * @param array|string $type
  189. * @param bool $callOriginalConstructor
  190. * @param bool $callAutoload
  191. * @param array $arguments
  192. * @param bool $callOriginalMethods
  193. * @param object $proxyTarget
  194. *
  195. * @return MockObject
  196. *
  197. * @throws \ReflectionException
  198. * @throws RuntimeException
  199. */
  200. private function getObject($code, $className, $type = '', $callOriginalConstructor = false, $callAutoload = false, array $arguments = [], $callOriginalMethods = false, $proxyTarget = null)
  201. {
  202. $this->evalClass($code, $className);
  203. if ($callOriginalConstructor &&
  204. \is_string($type) &&
  205. !\interface_exists($type, $callAutoload)) {
  206. if (\count($arguments) === 0) {
  207. $object = new $className;
  208. } else {
  209. $class = new ReflectionClass($className);
  210. $object = $class->newInstanceArgs($arguments);
  211. }
  212. } else {
  213. try {
  214. $instantiator = new Instantiator;
  215. $object = $instantiator->instantiate($className);
  216. } catch (InstantiatorException $exception) {
  217. throw new RuntimeException($exception->getMessage());
  218. }
  219. }
  220. if ($callOriginalMethods) {
  221. if (!\is_object($proxyTarget)) {
  222. if (\count($arguments) === 0) {
  223. $proxyTarget = new $type;
  224. } else {
  225. $class = new ReflectionClass($type);
  226. $proxyTarget = $class->newInstanceArgs($arguments);
  227. }
  228. }
  229. $object->__phpunit_setOriginalObject($proxyTarget);
  230. }
  231. return $object;
  232. }
  233. /**
  234. * @param string $code
  235. * @param string $className
  236. */
  237. private function evalClass($code, $className)
  238. {
  239. if (!\class_exists($className, false)) {
  240. eval($code);
  241. }
  242. }
  243. /**
  244. * Returns a mock object for the specified abstract class with all abstract
  245. * methods of the class mocked. Concrete methods to mock can be specified with
  246. * the last parameter
  247. *
  248. * @param string $originalClassName
  249. * @param array $arguments
  250. * @param string $mockClassName
  251. * @param bool $callOriginalConstructor
  252. * @param bool $callOriginalClone
  253. * @param bool $callAutoload
  254. * @param array $mockedMethods
  255. * @param bool $cloneArguments
  256. *
  257. * @return MockObject
  258. *
  259. * @throws \ReflectionException
  260. * @throws RuntimeException
  261. * @throws Exception
  262. */
  263. public function getMockForAbstractClass($originalClassName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true)
  264. {
  265. if (!\is_string($originalClassName)) {
  266. throw InvalidArgumentHelper::factory(1, 'string');
  267. }
  268. if (!\is_string($mockClassName)) {
  269. throw InvalidArgumentHelper::factory(3, 'string');
  270. }
  271. if (\class_exists($originalClassName, $callAutoload) ||
  272. \interface_exists($originalClassName, $callAutoload)) {
  273. $reflector = new ReflectionClass($originalClassName);
  274. $methods = $mockedMethods;
  275. foreach ($reflector->getMethods() as $method) {
  276. if ($method->isAbstract() && !\in_array($method->getName(), $methods)) {
  277. $methods[] = $method->getName();
  278. }
  279. }
  280. if (empty($methods)) {
  281. $methods = null;
  282. }
  283. return $this->getMock(
  284. $originalClassName,
  285. $methods,
  286. $arguments,
  287. $mockClassName,
  288. $callOriginalConstructor,
  289. $callOriginalClone,
  290. $callAutoload,
  291. $cloneArguments
  292. );
  293. }
  294. throw new RuntimeException(
  295. \sprintf('Class "%s" does not exist.', $originalClassName)
  296. );
  297. }
  298. /**
  299. * Returns a mock object for the specified trait with all abstract methods
  300. * of the trait mocked. Concrete methods to mock can be specified with the
  301. * `$mockedMethods` parameter.
  302. *
  303. * @param string $traitName
  304. * @param array $arguments
  305. * @param string $mockClassName
  306. * @param bool $callOriginalConstructor
  307. * @param bool $callOriginalClone
  308. * @param bool $callAutoload
  309. * @param array $mockedMethods
  310. * @param bool $cloneArguments
  311. *
  312. * @return MockObject
  313. *
  314. * @throws \ReflectionException
  315. * @throws RuntimeException
  316. * @throws Exception
  317. */
  318. public function getMockForTrait($traitName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true)
  319. {
  320. if (!\is_string($traitName)) {
  321. throw InvalidArgumentHelper::factory(1, 'string');
  322. }
  323. if (!\is_string($mockClassName)) {
  324. throw InvalidArgumentHelper::factory(3, 'string');
  325. }
  326. if (!\trait_exists($traitName, $callAutoload)) {
  327. throw new RuntimeException(
  328. \sprintf(
  329. 'Trait "%s" does not exist.',
  330. $traitName
  331. )
  332. );
  333. }
  334. $className = $this->generateClassName(
  335. $traitName,
  336. '',
  337. 'Trait_'
  338. );
  339. $classTemplate = $this->getTemplate('trait_class.tpl');
  340. $classTemplate->setVar(
  341. [
  342. 'prologue' => 'abstract ',
  343. 'class_name' => $className['className'],
  344. 'trait_name' => $traitName
  345. ]
  346. );
  347. $this->evalClass(
  348. $classTemplate->render(),
  349. $className['className']
  350. );
  351. return $this->getMockForAbstractClass($className['className'], $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments);
  352. }
  353. /**
  354. * Returns an object for the specified trait.
  355. *
  356. * @param string $traitName
  357. * @param array $arguments
  358. * @param string $traitClassName
  359. * @param bool $callOriginalConstructor
  360. * @param bool $callOriginalClone
  361. * @param bool $callAutoload
  362. *
  363. * @return object
  364. *
  365. * @throws \ReflectionException
  366. * @throws RuntimeException
  367. * @throws Exception
  368. */
  369. public function getObjectForTrait($traitName, array $arguments = [], $traitClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true)
  370. {
  371. if (!\is_string($traitName)) {
  372. throw InvalidArgumentHelper::factory(1, 'string');
  373. }
  374. if (!\is_string($traitClassName)) {
  375. throw InvalidArgumentHelper::factory(3, 'string');
  376. }
  377. if (!\trait_exists($traitName, $callAutoload)) {
  378. throw new RuntimeException(
  379. \sprintf(
  380. 'Trait "%s" does not exist.',
  381. $traitName
  382. )
  383. );
  384. }
  385. $className = $this->generateClassName(
  386. $traitName,
  387. $traitClassName,
  388. 'Trait_'
  389. );
  390. $classTemplate = $this->getTemplate('trait_class.tpl');
  391. $classTemplate->setVar(
  392. [
  393. 'prologue' => '',
  394. 'class_name' => $className['className'],
  395. 'trait_name' => $traitName
  396. ]
  397. );
  398. return $this->getObject($classTemplate->render(), $className['className']);
  399. }
  400. /**
  401. * @param array|string $type
  402. * @param array $methods
  403. * @param string $mockClassName
  404. * @param bool $callOriginalClone
  405. * @param bool $callAutoload
  406. * @param bool $cloneArguments
  407. * @param bool $callOriginalMethods
  408. *
  409. * @return array
  410. *
  411. * @throws \ReflectionException
  412. * @throws \PHPUnit\Framework\MockObject\RuntimeException
  413. */
  414. public function generate($type, array $methods = null, $mockClassName = '', $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false)
  415. {
  416. if (\is_array($type)) {
  417. \sort($type);
  418. }
  419. if ($mockClassName === '') {
  420. $key = \md5(
  421. \is_array($type) ? \implode('_', $type) : $type .
  422. \serialize($methods) .
  423. \serialize($callOriginalClone) .
  424. \serialize($cloneArguments) .
  425. \serialize($callOriginalMethods)
  426. );
  427. if (isset(self::$cache[$key])) {
  428. return self::$cache[$key];
  429. }
  430. }
  431. $mock = $this->generateMock(
  432. $type,
  433. $methods,
  434. $mockClassName,
  435. $callOriginalClone,
  436. $callAutoload,
  437. $cloneArguments,
  438. $callOriginalMethods
  439. );
  440. if (isset($key)) {
  441. self::$cache[$key] = $mock;
  442. }
  443. return $mock;
  444. }
  445. /**
  446. * @param string $wsdlFile
  447. * @param string $className
  448. * @param array $methods
  449. * @param array $options
  450. *
  451. * @return string
  452. *
  453. * @throws RuntimeException
  454. */
  455. public function generateClassFromWsdl($wsdlFile, $className, array $methods = [], array $options = [])
  456. {
  457. if (!\extension_loaded('soap')) {
  458. throw new RuntimeException(
  459. 'The SOAP extension is required to generate a mock object from WSDL.'
  460. );
  461. }
  462. $options = \array_merge($options, ['cache_wsdl' => WSDL_CACHE_NONE]);
  463. $client = new SoapClient($wsdlFile, $options);
  464. $_methods = \array_unique($client->__getFunctions());
  465. unset($client);
  466. \sort($_methods);
  467. $methodTemplate = $this->getTemplate('wsdl_method.tpl');
  468. $methodsBuffer = '';
  469. foreach ($_methods as $method) {
  470. $nameStart = \strpos($method, ' ') + 1;
  471. $nameEnd = \strpos($method, '(');
  472. $name = \substr($method, $nameStart, $nameEnd - $nameStart);
  473. if (empty($methods) || \in_array($name, $methods)) {
  474. $args = \explode(
  475. ',',
  476. \substr(
  477. $method,
  478. $nameEnd + 1,
  479. \strpos($method, ')') - $nameEnd - 1
  480. )
  481. );
  482. foreach (\range(0, \count($args) - 1) as $i) {
  483. $args[$i] = \substr($args[$i], \strpos($args[$i], '$'));
  484. }
  485. $methodTemplate->setVar(
  486. [
  487. 'method_name' => $name,
  488. 'arguments' => \implode(', ', $args)
  489. ]
  490. );
  491. $methodsBuffer .= $methodTemplate->render();
  492. }
  493. }
  494. $optionsBuffer = 'array(';
  495. foreach ($options as $key => $value) {
  496. $optionsBuffer .= $key . ' => ' . $value;
  497. }
  498. $optionsBuffer .= ')';
  499. $classTemplate = $this->getTemplate('wsdl_class.tpl');
  500. $namespace = '';
  501. if (\strpos($className, '\\') !== false) {
  502. $parts = \explode('\\', $className);
  503. $className = \array_pop($parts);
  504. $namespace = 'namespace ' . \implode('\\', $parts) . ';' . "\n\n";
  505. }
  506. $classTemplate->setVar(
  507. [
  508. 'namespace' => $namespace,
  509. 'class_name' => $className,
  510. 'wsdl' => $wsdlFile,
  511. 'options' => $optionsBuffer,
  512. 'methods' => $methodsBuffer
  513. ]
  514. );
  515. return $classTemplate->render();
  516. }
  517. /**
  518. * @param array|string $type
  519. * @param array|null $methods
  520. * @param string $mockClassName
  521. * @param bool $callOriginalClone
  522. * @param bool $callAutoload
  523. * @param bool $cloneArguments
  524. * @param bool $callOriginalMethods
  525. *
  526. * @return array
  527. *
  528. * @throws \InvalidArgumentException
  529. * @throws \ReflectionException
  530. * @throws RuntimeException
  531. */
  532. private function generateMock($type, $methods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods)
  533. {
  534. $methodReflections = [];
  535. $classTemplate = $this->getTemplate('mocked_class.tpl');
  536. $additionalInterfaces = [];
  537. $cloneTemplate = '';
  538. $isClass = false;
  539. $isInterface = false;
  540. $isMultipleInterfaces = false;
  541. if (\is_array($type)) {
  542. foreach ($type as $_type) {
  543. if (!\interface_exists($_type, $callAutoload)) {
  544. throw new RuntimeException(
  545. \sprintf(
  546. 'Interface "%s" does not exist.',
  547. $_type
  548. )
  549. );
  550. }
  551. $isMultipleInterfaces = true;
  552. $additionalInterfaces[] = $_type;
  553. $typeClass = new ReflectionClass($this->generateClassName(
  554. $_type,
  555. $mockClassName,
  556. 'Mock_'
  557. )['fullClassName']
  558. );
  559. foreach ($this->getClassMethods($_type) as $method) {
  560. if (\in_array($method, $methods)) {
  561. throw new RuntimeException(
  562. \sprintf(
  563. 'Duplicate method "%s" not allowed.',
  564. $method
  565. )
  566. );
  567. }
  568. $methodReflections[$method] = $typeClass->getMethod($method);
  569. $methods[] = $method;
  570. }
  571. }
  572. }
  573. $mockClassName = $this->generateClassName(
  574. $type,
  575. $mockClassName,
  576. 'Mock_'
  577. );
  578. if (\class_exists($mockClassName['fullClassName'], $callAutoload)) {
  579. $isClass = true;
  580. } elseif (\interface_exists($mockClassName['fullClassName'], $callAutoload)) {
  581. $isInterface = true;
  582. }
  583. if (!$isClass && !$isInterface) {
  584. $prologue = 'class ' . $mockClassName['originalClassName'] . "\n{\n}\n\n";
  585. if (!empty($mockClassName['namespaceName'])) {
  586. $prologue = 'namespace ' . $mockClassName['namespaceName'] .
  587. " {\n\n" . $prologue . "}\n\n" .
  588. "namespace {\n\n";
  589. $epilogue = "\n\n}";
  590. }
  591. $cloneTemplate = $this->getTemplate('mocked_clone.tpl');
  592. } else {
  593. $class = new ReflectionClass($mockClassName['fullClassName']);
  594. if ($class->isFinal()) {
  595. throw new RuntimeException(
  596. \sprintf(
  597. 'Class "%s" is declared "final" and cannot be mocked.',
  598. $mockClassName['fullClassName']
  599. )
  600. );
  601. }
  602. if ($class->hasMethod('__clone')) {
  603. $cloneMethod = $class->getMethod('__clone');
  604. if (!$cloneMethod->isFinal()) {
  605. if ($callOriginalClone && !$isInterface) {
  606. $cloneTemplate = $this->getTemplate('unmocked_clone.tpl');
  607. } else {
  608. $cloneTemplate = $this->getTemplate('mocked_clone.tpl');
  609. }
  610. }
  611. } else {
  612. $cloneTemplate = $this->getTemplate('mocked_clone.tpl');
  613. }
  614. }
  615. if (\is_object($cloneTemplate)) {
  616. $cloneTemplate = $cloneTemplate->render();
  617. }
  618. if (\is_array($methods) && empty($methods) &&
  619. ($isClass || $isInterface)) {
  620. $methods = $this->getClassMethods($mockClassName['fullClassName']);
  621. }
  622. if (!\is_array($methods)) {
  623. $methods = [];
  624. }
  625. $mockedMethods = '';
  626. $configurable = [];
  627. foreach ($methods as $methodName) {
  628. if ($methodName !== '__construct' && $methodName !== '__clone') {
  629. $configurable[] = \strtolower($methodName);
  630. }
  631. }
  632. if (isset($class)) {
  633. // https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103
  634. if ($isInterface && $class->implementsInterface(Traversable::class) &&
  635. !$class->implementsInterface(Iterator::class) &&
  636. !$class->implementsInterface(IteratorAggregate::class)) {
  637. $additionalInterfaces[] = Iterator::class;
  638. $methods = \array_merge($methods, $this->getClassMethods(Iterator::class));
  639. }
  640. foreach ($methods as $methodName) {
  641. try {
  642. $method = $class->getMethod($methodName);
  643. if ($this->canMockMethod($method)) {
  644. $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting(
  645. $method,
  646. $cloneArguments,
  647. $callOriginalMethods
  648. );
  649. }
  650. } catch (ReflectionException $e) {
  651. $mockedMethods .= $this->generateMockedMethodDefinition(
  652. $mockClassName['fullClassName'],
  653. $methodName,
  654. $cloneArguments
  655. );
  656. }
  657. }
  658. } elseif ($isMultipleInterfaces) {
  659. foreach ($methods as $methodName) {
  660. if ($this->canMockMethod($methodReflections[$methodName])) {
  661. $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting(
  662. $methodReflections[$methodName],
  663. $cloneArguments,
  664. $callOriginalMethods
  665. );
  666. }
  667. }
  668. } else {
  669. foreach ($methods as $methodName) {
  670. $mockedMethods .= $this->generateMockedMethodDefinition(
  671. $mockClassName['fullClassName'],
  672. $methodName,
  673. $cloneArguments
  674. );
  675. }
  676. }
  677. $method = '';
  678. if (!\in_array('method', $methods) && (!isset($class) || !$class->hasMethod('method'))) {
  679. $methodTemplate = $this->getTemplate('mocked_class_method.tpl');
  680. $method = $methodTemplate->render();
  681. }
  682. $classTemplate->setVar(
  683. [
  684. 'prologue' => $prologue ?? '',
  685. 'epilogue' => $epilogue ?? '',
  686. 'class_declaration' => $this->generateMockClassDeclaration(
  687. $mockClassName,
  688. $isInterface,
  689. $additionalInterfaces
  690. ),
  691. 'clone' => $cloneTemplate,
  692. 'mock_class_name' => $mockClassName['className'],
  693. 'mocked_methods' => $mockedMethods,
  694. 'method' => $method,
  695. 'configurable' => '[' . \implode(', ', \array_map(function ($m) {
  696. return '\'' . $m . '\'';
  697. }, $configurable)) . ']'
  698. ]
  699. );
  700. return [
  701. 'code' => $classTemplate->render(),
  702. 'mockClassName' => $mockClassName['className']
  703. ];
  704. }
  705. /**
  706. * @param array|string $type
  707. * @param string $className
  708. * @param string $prefix
  709. *
  710. * @return array
  711. */
  712. private function generateClassName($type, $className, $prefix)
  713. {
  714. if (\is_array($type)) {
  715. $type = \implode('_', $type);
  716. }
  717. if ($type[0] === '\\') {
  718. $type = \substr($type, 1);
  719. }
  720. $classNameParts = \explode('\\', $type);
  721. if (\count($classNameParts) > 1) {
  722. $type = \array_pop($classNameParts);
  723. $namespaceName = \implode('\\', $classNameParts);
  724. $fullClassName = $namespaceName . '\\' . $type;
  725. } else {
  726. $namespaceName = '';
  727. $fullClassName = $type;
  728. }
  729. if ($className === '') {
  730. do {
  731. $className = $prefix . $type . '_' .
  732. \substr(\md5(\mt_rand()), 0, 8);
  733. } while (\class_exists($className, false));
  734. }
  735. return [
  736. 'className' => $className,
  737. 'originalClassName' => $type,
  738. 'fullClassName' => $fullClassName,
  739. 'namespaceName' => $namespaceName
  740. ];
  741. }
  742. /**
  743. * @param array $mockClassName
  744. * @param bool $isInterface
  745. * @param array $additionalInterfaces
  746. *
  747. * @return string
  748. */
  749. private function generateMockClassDeclaration(array $mockClassName, $isInterface, array $additionalInterfaces = [])
  750. {
  751. $buffer = 'class ';
  752. $additionalInterfaces[] = MockObject::class;
  753. $interfaces = \implode(', ', $additionalInterfaces);
  754. if ($isInterface) {
  755. $buffer .= \sprintf(
  756. '%s implements %s',
  757. $mockClassName['className'],
  758. $interfaces
  759. );
  760. if (!\in_array($mockClassName['originalClassName'], $additionalInterfaces)) {
  761. $buffer .= ', ';
  762. if (!empty($mockClassName['namespaceName'])) {
  763. $buffer .= $mockClassName['namespaceName'] . '\\';
  764. }
  765. $buffer .= $mockClassName['originalClassName'];
  766. }
  767. } else {
  768. $buffer .= \sprintf(
  769. '%s extends %s%s implements %s',
  770. $mockClassName['className'],
  771. !empty($mockClassName['namespaceName']) ? $mockClassName['namespaceName'] . '\\' : '',
  772. $mockClassName['originalClassName'],
  773. $interfaces
  774. );
  775. }
  776. return $buffer;
  777. }
  778. /**
  779. * @param ReflectionMethod $method
  780. * @param bool $cloneArguments
  781. * @param bool $callOriginalMethods
  782. *
  783. * @return string
  784. *
  785. * @throws \PHPUnit\Framework\MockObject\RuntimeException
  786. */
  787. private function generateMockedMethodDefinitionFromExisting(ReflectionMethod $method, $cloneArguments, $callOriginalMethods)
  788. {
  789. if ($method->isPrivate()) {
  790. $modifier = 'private';
  791. } elseif ($method->isProtected()) {
  792. $modifier = 'protected';
  793. } else {
  794. $modifier = 'public';
  795. }
  796. if ($method->isStatic()) {
  797. $modifier .= ' static';
  798. }
  799. if ($method->returnsReference()) {
  800. $reference = '&';
  801. } else {
  802. $reference = '';
  803. }
  804. if ($method->hasReturnType()) {
  805. $returnType = (string) $method->getReturnType();
  806. } else {
  807. $returnType = '';
  808. }
  809. if (\preg_match('#\*[ \t]*+@deprecated[ \t]*+(.*?)\r?+\n[ \t]*+\*(?:[ \t]*+@|/$)#s', $method->getDocComment(), $deprecation)) {
  810. $deprecation = \trim(\preg_replace('#[ \t]*\r?\n[ \t]*+\*[ \t]*+#', ' ', $deprecation[1]));
  811. } else {
  812. $deprecation = false;
  813. }
  814. return $this->generateMockedMethodDefinition(
  815. $method->getDeclaringClass()->getName(),
  816. $method->getName(),
  817. $cloneArguments,
  818. $modifier,
  819. $this->getMethodParameters($method),
  820. $this->getMethodParameters($method, true),
  821. $returnType,
  822. $reference,
  823. $callOriginalMethods,
  824. $method->isStatic(),
  825. $deprecation,
  826. $method->hasReturnType() && PHP_VERSION_ID >= 70100 && $method->getReturnType()->allowsNull()
  827. );
  828. }
  829. /**
  830. * @param string $className
  831. * @param string $methodName
  832. * @param bool $cloneArguments
  833. * @param string $modifier
  834. * @param string $argumentsForDeclaration
  835. * @param string $argumentsForCall
  836. * @param string $returnType
  837. * @param string $reference
  838. * @param bool $callOriginalMethods
  839. * @param bool $static
  840. * @param bool|string $deprecation
  841. * @param bool $allowsReturnNull
  842. *
  843. * @return string
  844. *
  845. * @throws \InvalidArgumentException
  846. */
  847. private function generateMockedMethodDefinition($className, $methodName, $cloneArguments = true, $modifier = 'public', $argumentsForDeclaration = '', $argumentsForCall = '', $returnType = '', $reference = '', $callOriginalMethods = false, $static = false, $deprecation = false, $allowsReturnNull = false)
  848. {
  849. if ($static) {
  850. $templateFile = 'mocked_static_method.tpl';
  851. } else {
  852. if ($returnType === 'void') {
  853. $templateFile = \sprintf(
  854. '%s_method_void.tpl',
  855. $callOriginalMethods ? 'proxied' : 'mocked'
  856. );
  857. } else {
  858. $templateFile = \sprintf(
  859. '%s_method.tpl',
  860. $callOriginalMethods ? 'proxied' : 'mocked'
  861. );
  862. }
  863. }
  864. // Mocked interfaces returning 'self' must explicitly declare the
  865. // interface name as the return type. See
  866. // https://bugs.php.net/bug.php?id=70722
  867. if ($returnType === 'self') {
  868. $returnType = $className;
  869. }
  870. if (false !== $deprecation) {
  871. $deprecation = "The $className::$methodName method is deprecated ($deprecation).";
  872. $deprecationTemplate = $this->getTemplate('deprecation.tpl');
  873. $deprecationTemplate->setVar(
  874. [
  875. 'deprecation' => \var_export($deprecation, true),
  876. ]
  877. );
  878. $deprecation = $deprecationTemplate->render();
  879. }
  880. $template = $this->getTemplate($templateFile);
  881. $template->setVar(
  882. [
  883. 'arguments_decl' => $argumentsForDeclaration,
  884. 'arguments_call' => $argumentsForCall,
  885. 'return_delim' => $returnType ? ': ' : '',
  886. 'return_type' => $allowsReturnNull ? '?' . $returnType : $returnType,
  887. 'arguments_count' => !empty($argumentsForCall) ? \substr_count($argumentsForCall, ',') + 1 : 0,
  888. 'class_name' => $className,
  889. 'method_name' => $methodName,
  890. 'modifier' => $modifier,
  891. 'reference' => $reference,
  892. 'clone_arguments' => $cloneArguments ? 'true' : 'false',
  893. 'deprecation' => $deprecation
  894. ]
  895. );
  896. return $template->render();
  897. }
  898. /**
  899. * @param ReflectionMethod $method
  900. *
  901. * @return bool
  902. *
  903. * @throws \ReflectionException
  904. */
  905. private function canMockMethod(ReflectionMethod $method)
  906. {
  907. return !($method->isConstructor() || $method->isFinal() || $method->isPrivate() || $this->isMethodNameBlacklisted($method->getName()));
  908. }
  909. /**
  910. * Returns whether a method name is blacklisted
  911. *
  912. * @param string $name
  913. *
  914. * @return bool
  915. */
  916. private function isMethodNameBlacklisted($name)
  917. {
  918. return isset($this->blacklistedMethodNames[$name]);
  919. }
  920. /**
  921. * Returns the parameters of a function or method.
  922. *
  923. * @param ReflectionMethod $method
  924. * @param bool $forCall
  925. *
  926. * @return string
  927. *
  928. * @throws RuntimeException
  929. */
  930. private function getMethodParameters(ReflectionMethod $method, $forCall = false)
  931. {
  932. $parameters = [];
  933. foreach ($method->getParameters() as $i => $parameter) {
  934. $name = '$' . $parameter->getName();
  935. /* Note: PHP extensions may use empty names for reference arguments
  936. * or "..." for methods taking a variable number of arguments.
  937. */
  938. if ($name === '$' || $name === '$...') {
  939. $name = '$arg' . $i;
  940. }
  941. if ($parameter->isVariadic()) {
  942. if ($forCall) {
  943. continue;
  944. }
  945. $name = '...' . $name;
  946. }
  947. $nullable = '';
  948. $default = '';
  949. $reference = '';
  950. $typeDeclaration = '';
  951. if (!$forCall) {
  952. if (PHP_VERSION_ID >= 70100 && $parameter->hasType() && $parameter->allowsNull()) {
  953. $nullable = '?';
  954. }
  955. if ($parameter->hasType() && (string) $parameter->getType() !== 'self') {
  956. $typeDeclaration = (string) $parameter->getType() . ' ';
  957. } elseif ($parameter->isArray()) {
  958. $typeDeclaration = 'array ';
  959. } elseif ($parameter->isCallable()) {
  960. $typeDeclaration = 'callable ';
  961. } else {
  962. try {
  963. $class = $parameter->getClass();
  964. } catch (ReflectionException $e) {
  965. throw new RuntimeException(
  966. \sprintf(
  967. 'Cannot mock %s::%s() because a class or ' .
  968. 'interface used in the signature is not loaded',
  969. $method->getDeclaringClass()->getName(),
  970. $method->getName()
  971. ),
  972. 0,
  973. $e
  974. );
  975. }
  976. if ($class !== null) {
  977. $typeDeclaration = $class->getName() . ' ';
  978. }
  979. }
  980. if (!$parameter->isVariadic()) {
  981. if ($parameter->isDefaultValueAvailable()) {
  982. $value = $parameter->getDefaultValueConstantName();
  983. if ($value === null) {
  984. $value = \var_export($parameter->getDefaultValue(), true);
  985. } elseif (!\defined($value)) {
  986. $rootValue = \preg_replace('/^.*\\\\/', '', $value);
  987. $value = \defined($rootValue) ? $rootValue : $value;
  988. }
  989. $default = ' = ' . $value;
  990. } elseif ($parameter->isOptional()) {
  991. $default = ' = null';
  992. }
  993. }
  994. }
  995. if ($parameter->isPassedByReference()) {
  996. $reference = '&';
  997. }
  998. $parameters[] = $nullable . $typeDeclaration . $reference . $name . $default;
  999. }
  1000. return \implode(', ', $parameters);
  1001. }
  1002. /**
  1003. * @param string $className
  1004. *
  1005. * @return array
  1006. *
  1007. * @throws \ReflectionException
  1008. */
  1009. public function getClassMethods($className)
  1010. {
  1011. $class = new ReflectionClass($className);
  1012. $methods = [];
  1013. foreach ($class->getMethods() as $method) {
  1014. if ($method->isPublic() || $method->isAbstract()) {
  1015. $methods[] = $method->getName();
  1016. }
  1017. }
  1018. return $methods;
  1019. }
  1020. /**
  1021. * @param string $template
  1022. *
  1023. * @return Text_Template
  1024. *
  1025. * @throws \InvalidArgumentException
  1026. */
  1027. private function getTemplate($template)
  1028. {
  1029. $filename = __DIR__ . DIRECTORY_SEPARATOR . 'Generator' . DIRECTORY_SEPARATOR . $template;
  1030. if (!isset(self::$templates[$filename])) {
  1031. self::$templates[$filename] = new Text_Template($filename);
  1032. }
  1033. return self::$templates[$filename];
  1034. }
  1035. }