PageRenderTime 52ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Nunzion/Expect.php

https://bitbucket.org/nunzion/php-expect
PHP | 750 lines | 445 code | 89 blank | 216 comment | 58 complexity | fb7564c74e2e19c68ef4268edb826728 MD5 | raw file
  1. <?php
  2. /**
  3. * @author Henning Dieterichs <henning.dieterichs@hediet.de>
  4. * @copyright 2013-2014 Henning Dieterichs <henning.dieterichs@hediet.de>
  5. * @license http://opensource.org/licenses/MIT MIT
  6. */
  7. namespace Nunzion;
  8. /**
  9. * Defines methods to ensure expectations.
  10. */
  11. class Expect
  12. {
  13. /**
  14. * Returns a new expect object to ensure expectations.
  15. *
  16. * @param mixed $value the value to ensure expectations for.
  17. * @return Expect the expect object.
  18. */
  19. public static function that($value)
  20. {
  21. return new static($value, "", null, true);
  22. }
  23. /**
  24. * The value to ensure expectations for.
  25. * @var mixed
  26. */
  27. private $value;
  28. /**
  29. * Is true, if the value is defined, otherwise false.
  30. * Is only false for results of itsArrayElement() if the key is not defined.
  31. * @var bool
  32. */
  33. private $isDefined;
  34. /**
  35. * The key of value. Only set for results of itsArrayElement().
  36. * @var string
  37. */
  38. private $key;
  39. /**
  40. * The path to this element.
  41. * Is only different from "" for results of itsArrayElement().
  42. * @var string
  43. */
  44. private $path;
  45. private function __construct($value, $path, $key, $isDefined)
  46. {
  47. $this->value = $value;
  48. $this->path = $path;
  49. $this->key = $key;
  50. $this->isDefined = $isDefined;
  51. }
  52. // <editor-fold defaultstate="collapsed" desc="General tests">
  53. /**
  54. * Throws an appropiate exception, depending on the current test subject.
  55. * This method does not perform any tests!
  56. *
  57. * @param string $message the message of the exception
  58. * @param array $arguments the arguments used within message.
  59. */
  60. public function _($message, $arguments = null)
  61. {
  62. $e = $this->getExceptionConstructor($message, $arguments);
  63. throw call_user_func_array($e[0], $e[1]);
  64. }
  65. /**
  66. * Ensures that the value is defined and the same as $other.
  67. * Two objects are the same, if the '===' comparison succeeds.
  68. *
  69. * @param mixed $other the reference value.
  70. * @return self
  71. */
  72. public function isTheSameAs($other)
  73. {
  74. $this->isDefined();
  75. if ($this->value !== $other)
  76. {
  77. $e = $this->getUnexpectedValueExceptionConstructor("must be the same as {expected}", array("expected" => $other));
  78. throw call_user_func_array($e[0], $e[1]);
  79. }
  80. return $this;
  81. }
  82. /**
  83. * Ensures that the value is defined and equal to $other.
  84. * Two objects are equal, if the '==' comparison suceeds.
  85. *
  86. * @param mixed $other the reference value.
  87. * @return self
  88. */
  89. public function equals($other)
  90. {
  91. $this->isDefined();
  92. if ($this->value != $other)
  93. {
  94. $e = $this->getUnexpectedValueExceptionConstructor("must be equal to {expected}", array("expected" => $other));
  95. throw call_user_func_array($e[0], $e[1]);
  96. }
  97. return $this;
  98. }
  99. /**
  100. * Ensures that the value is defined and null.
  101. *
  102. * @return self
  103. */
  104. public function isNull()
  105. {
  106. $this->isDefined();
  107. if ($this->value !== null)
  108. {
  109. $e = $this->getUnexpectedValueExceptionConstructor("must be null", array("expected" => null));
  110. throw call_user_func_array($e[0], $e[1]);
  111. }
  112. return $this;
  113. }
  114. /**
  115. * Ensures the the value is not null.
  116. *
  117. * @return self
  118. */
  119. public function isNotNull()
  120. {
  121. if ($this->value === null)
  122. {
  123. $e = $this->getExceptionConstructor("cannot be null");
  124. throw call_user_func_array($e[0], $e[1]);
  125. }
  126. return $this;
  127. }
  128. /**
  129. * Ensures the the value is defined and empty.
  130. *
  131. * @return self
  132. */
  133. public function isEmpty()
  134. {
  135. $this->isDefined();
  136. if (!empty($this->value))
  137. {
  138. $e = $this->getUnexpectedValueExceptionConstructor("must be empty");
  139. throw call_user_func_array($e[0], $e[1]);
  140. }
  141. return $this;
  142. }
  143. /**
  144. * Ensures the the value is not empty.
  145. *
  146. * @return self
  147. */
  148. public function isNotEmpty()
  149. {
  150. if (empty($this->value))
  151. {
  152. $e = $this->getExceptionConstructor("cannot be empty");
  153. throw call_user_func_array($e[0], $e[1]);
  154. }
  155. return $this;
  156. }
  157. // </editor-fold>
  158. // <editor-fold defaultstate="collapsed" desc="Type tests">
  159. /**
  160. * Ensures that the value is of the given type.
  161. * If an appropriate "is*" method exists, it will be called,
  162. * otherwise isInstanceOf will be called.
  163. *
  164. * @param string $type the type.
  165. * @return self
  166. */
  167. public function is($type)
  168. {
  169. $supportedTests = array(
  170. "int" => "isInt",
  171. "number" => "isNumber",
  172. "float" => "isFloat",
  173. "string" => "isString",
  174. "array" => "isArray",
  175. "callable" => "isCallable",
  176. "object" => "isObject");
  177. if (array_key_exists($type, $supportedTests))
  178. {
  179. $m = $supportedTests[$type];
  180. $this->$m();
  181. }
  182. else
  183. {
  184. $this->isInstanceOf($type);
  185. }
  186. return $this;
  187. }
  188. /**
  189. * Ensures that the value is an integer.
  190. *
  191. * @return self
  192. */
  193. public function isInt()
  194. {
  195. if (!is_int($this->value))
  196. {
  197. $e = $this->getTypeMismatchExceptionConstructor("int");
  198. throw call_user_func_array($e[0], $e[1]);
  199. }
  200. return $this;
  201. }
  202. /**
  203. * Ensures that the value is a float.
  204. *
  205. * @return self
  206. */
  207. public function isFloat()
  208. {
  209. if (!is_float($this->value))
  210. {
  211. $e = $this->getTypeMismatchExceptionConstructor("float");
  212. throw call_user_func_array($e[0], $e[1]);
  213. }
  214. return $this;
  215. }
  216. /**
  217. * Ensures that the value is a number, i.e. either an integer or a float.
  218. *
  219. * @return self
  220. */
  221. public function isNumber()
  222. {
  223. if (!is_float($this->value) && !is_int($this->value))
  224. {
  225. $e = $this->getTypeMismatchExceptionConstructor("number");
  226. throw call_user_func_array($e[0], $e[1]);
  227. }
  228. return $this;
  229. }
  230. /**
  231. * Ensures that the value is a string.
  232. *
  233. * @return self
  234. */
  235. public function isString()
  236. {
  237. if (!is_string($this->value))
  238. {
  239. $e = $this->getTypeMismatchExceptionConstructor("string");
  240. throw call_user_func_array($e[0], $e[1]);
  241. }
  242. return $this;
  243. }
  244. /**
  245. * Ensures that the value is callable.
  246. *
  247. * @return self
  248. */
  249. public function isCallable()
  250. {
  251. if (!is_callable($this->value))
  252. {
  253. $e = $this->getTypeMismatchExceptionConstructor("callable");
  254. throw call_user_func_array($e[0], $e[1]);
  255. }
  256. return $this;
  257. }
  258. /**
  259. * Ensures that the value is an array.
  260. *
  261. * @return self
  262. */
  263. public function isArray()
  264. {
  265. if (!is_array($this->value))
  266. {
  267. $e = $this->getTypeMismatchExceptionConstructor("array");
  268. throw call_user_func_array($e[0], $e[1]);
  269. }
  270. return $this;
  271. }
  272. /**
  273. * Ensures that the value is an array whose items are type of $itemType.
  274. * For each item, the method "is" will be called.
  275. *
  276. * @param string $itemType the type of the items.
  277. * @return self
  278. */
  279. public function isArrayOf($itemType)
  280. {
  281. if (!is_array($this->value))
  282. {
  283. $e = $this->getTypeMismatchExceptionConstructor($itemType . "[]");
  284. throw call_user_func_array($e[0], $e[1]);
  285. }
  286. foreach ($this->value as $key => $item)
  287. {
  288. //throw exception with appropriate error message
  289. $this->itsArrayElement($key)->is($itemType);
  290. }
  291. return $this;
  292. }
  293. /**
  294. * Ensures that the value is an object.
  295. *
  296. * @return self
  297. */
  298. public function isObject()
  299. {
  300. if (!is_object($this->value))
  301. {
  302. $e = $this->getTypeMismatchExceptionConstructor("object");
  303. throw call_user_func_array($e[0], $e[1]);
  304. }
  305. return $this;
  306. }
  307. /**
  308. * Ensures that the value is of type $classOrInterface.
  309. *
  310. * @param string $classOrInterface the class or interface name.
  311. * @return self
  312. */
  313. public function isInstanceOf($classOrInterface)
  314. {
  315. if (!($this->value instanceof $classOrInterface))
  316. {
  317. $message = class_exists($classOrInterface, false) ?
  318. "must be an instance of" : "must implement";
  319. $message .= " '{expected}', but is {actualText}";
  320. $e = $this->getTypeMismatchExceptionConstructor($classOrInterface, $message);
  321. throw call_user_func_array($e[0], $e[1]);
  322. }
  323. return $this;
  324. }
  325. // </editor-fold>
  326. // <editor-fold defaultstate="collapsed" desc="Number tests">
  327. /**
  328. * Ensures that the value is a number and in the interval [$min, $max].
  329. *
  330. * @param int|float $min the lower bound, inclusive.
  331. * @param int|float $max the upper bound, inclusive.
  332. * @return self
  333. */
  334. public function isBetween($min, $max)
  335. {
  336. $this->isNumber();
  337. if ($this->value < $min || $this->value > $max)
  338. {
  339. $e = $this->getUnexpectedValueExceptionConstructor("must be between {min} and {max}", array("min" => $min, "max" => $max));
  340. throw call_user_func_array($e[0], $e[1]);
  341. }
  342. return $this;
  343. }
  344. /**
  345. * Ensures that the value is a number and greater than $other.
  346. *
  347. * @param int|float $other the reference value.
  348. * @return self
  349. */
  350. public function isGreaterThan($other)
  351. {
  352. $this->isNumber();
  353. if ($this->value <= $other)
  354. {
  355. $e = $this->getUnexpectedValueExceptionConstructor("must be greater than {other}", array("other" => $other));
  356. throw call_user_func_array($e[0], $e[1]);
  357. }
  358. return $this;
  359. }
  360. /**
  361. * Ensures that the value is a number and less than $other.
  362. *
  363. * @param int|float $other the reference value.
  364. * @return self
  365. */
  366. public function isLessThan($other)
  367. {
  368. $this->isNumber();
  369. if ($this->value >= $other)
  370. {
  371. $e = $this->getUnexpectedValueExceptionConstructor("must be less than {other}", array("other" => $other));
  372. throw call_user_func_array($e[0], $e[1]);
  373. }
  374. return $this;
  375. }
  376. /**
  377. * Ensures that the value is a number and greater than or equal to $other.
  378. *
  379. * @param int|float $other the reference value.
  380. * @return self
  381. */
  382. public function isGreaterThanOrEqualTo($other)
  383. {
  384. $this->isNumber();
  385. if ($this->value < $other)
  386. {
  387. $e = $this->getUnexpectedValueExceptionConstructor("must be greater or equal than {other}", array("other" => $other));
  388. throw call_user_func_array($e[0], $e[1]);
  389. }
  390. return $this;
  391. }
  392. /**
  393. * @deprecated since version 1.0.0
  394. * @see isGreaterThanOrEqualTo()
  395. */
  396. public function isGreaterOrEqualThan($other)
  397. {
  398. return $this->isGreaterThanOrEqualTo($other);
  399. }
  400. /**
  401. * Ensures that the value is a number and less than or equal to $other.
  402. *
  403. * @param int|float $other the reference value.
  404. * @return self
  405. */
  406. public function isLessThanOrEqualTo($other)
  407. {
  408. $this->isNumber();
  409. if ($this->value > $other)
  410. {
  411. $e = $this->getUnexpectedValueExceptionConstructor("must be less or equal than {other}", array("other" => $other));
  412. throw call_user_func_array($e[0], $e[1]);
  413. }
  414. return $this;
  415. }
  416. /**
  417. * @deprecated since version 1.0.0
  418. * @see isGreaterThanOrEqualTo()
  419. */
  420. public function isLessOrEqualThan($other)
  421. {
  422. return $this->isLessThanOrEqualTo($other);
  423. }
  424. // </editor-fold>
  425. /**
  426. * Checks whether the called method begins with "isNullOr" or "isUndefinedOr"
  427. * and calls the method with name "is" concatenized with the rest of the string.
  428. *
  429. * Example call: isNullOrString() or isNullOrBetween(1, 2)
  430. *
  431. * @param string $name
  432. * @param array $arguments
  433. * @return self
  434. */
  435. public function __call($name, array $arguments)
  436. {
  437. $isNullOr = "isNullOr";
  438. $isUndefinedOr = "isUndefinedOr";
  439. if (stripos($name, $isNullOr) === 0)
  440. {
  441. if ($this->isDefined && $this->value === null)
  442. return;
  443. $newMethod = "is" . substr($name, strlen($isNullOr));
  444. }
  445. else if (stripos($name, $isUndefinedOr) === 0)
  446. {
  447. if (!$this->isDefined)
  448. return;
  449. $newMethod = "is" . substr($name, strlen($isUndefinedOr));
  450. }
  451. else
  452. throw new \BadMethodCallException("Method '" . $name . "' does not exist.");
  453. //this is faster than call_user_func
  454. switch (count($arguments))
  455. {
  456. case 0: return $this->$newMethod();
  457. case 1: return $this->$newMethod($arguments[0]);
  458. case 2: return $this->$newMethod($arguments[0], $arguments[1]);
  459. case 3: return $this->$newMethod($arguments[0], $arguments[1], $arguments[2]);
  460. default: throw new \InvalidArgumentException("Too many arguments.");
  461. }
  462. return $this;
  463. }
  464. // <editor-fold defaultstate="collapsed" desc="Array tests">
  465. /**
  466. * Ensures the the value is defined. This can only fail for results of
  467. * itsArrayElement().
  468. *
  469. * @return self
  470. */
  471. public function isDefined()
  472. {
  473. if (!$this->isDefined)
  474. {
  475. $e = $this->getExceptionConstructor("must be defined");
  476. throw call_user_func_array($e[0], $e[1]);
  477. }
  478. return $this;
  479. }
  480. /**
  481. * Ensures that the value is an array and gets an expect object for an array element.
  482. *
  483. * @param mixed $key the key.
  484. * @return self
  485. */
  486. public function itsArrayElement($key)
  487. {
  488. $this->isArray();
  489. $isDefined = array_key_exists($key, $this->value);
  490. $value = $isDefined ? $this->value[$key] : null;
  491. return new static($value, $this->path . "[" . $key . "]", $key, $isDefined);
  492. }
  493. // </editor-fold>
  494. // <editor-fold defaultstate="collapsed" desc="Helper">
  495. /**
  496. * Checks whether $variableName is a parameter name.
  497. *
  498. * @param string $variableName the name of the variable.
  499. * @param array $callTraceElement the call trace element of the expect method call.
  500. * @return boolean true, if $variableName is a parameter, otherwise false.
  501. */
  502. private function isParameter($variableName, array $callTraceElement)
  503. {
  504. if (!isset($callTraceElement["function"]))
  505. return false;
  506. try
  507. {
  508. if (isset($callTraceElement["class"]))
  509. $m = new \ReflectionMethod($callTraceElement["class"], $callTraceElement["function"]);
  510. else
  511. $m = new \ReflectionFunction($callTraceElement["function"]);
  512. }
  513. catch (\ReflectionException $e)
  514. {
  515. return false;
  516. }
  517. if ($m !== null)
  518. {
  519. foreach ($m->getParameters() as $p)
  520. if ($p->name === $variableName)
  521. return true;
  522. }
  523. return false;
  524. }
  525. /**
  526. * Finds ::that(.*) in or before the line the is*-method is called from.
  527. * @param array $callTraceElement
  528. * @return string the parameter expression like "$this->foo", or null if it could not be found.
  529. */
  530. private function getThatParameterExpression(array $callTraceElement)
  531. {
  532. if (!isset($callTraceElement["file"]) || !isset($callTraceElement["line"]))
  533. return null;
  534. $lines = file($callTraceElement["file"]);
  535. $lineCount = count($lines);
  536. $functionCallLine = $callTraceElement["line"];
  537. if ($functionCallLine >= $lineCount)
  538. return null;
  539. for ($i = 1; $i <= $lineCount; $i++)
  540. {
  541. $line = $lines[$functionCallLine - $i];
  542. $matches = array();
  543. preg_match("/\\::that\\((?<parameter>.*?)\\)/", $line, $matches);
  544. if (count($matches) > 0)
  545. return trim($matches["parameter"]);
  546. }
  547. return null;
  548. }
  549. private function format($template, $arguments)
  550. {
  551. return preg_replace_callback('/\\{(?<parameterName>.*?)(\\:(?<formatOptions>.*))?\\}/',
  552. function ($match) use ($arguments)
  553. {
  554. $parameterName = $match["parameterName"];
  555. if (isset($arguments[$parameterName]))
  556. {
  557. $result = $arguments[$parameterName];
  558. if (is_object($result) || is_array($result))
  559. $result = print_r($result, true);
  560. return $result;
  561. }
  562. }, $template);
  563. }
  564. protected function getPreconditionViolationExceptionConstructor($message, $arguments)
  565. {
  566. return array(
  567. array(new \ReflectionClass("\InvalidArgumentException"), "newInstance"),
  568. array($message)
  569. );
  570. }
  571. protected function getInvariantViolationExceptionConstructor($message, $arguments)
  572. {
  573. return array(
  574. array(new \ReflectionClass("\Nunzion\InvariantViolationException"), "newInstance"),
  575. array($message)
  576. );
  577. }
  578. protected function getConditionViolationExceptionConstructor($message, $arguments)
  579. {
  580. return array(
  581. array(new \ReflectionClass("\UnexpectedValueException"), "newInstance"),
  582. array($message)
  583. );
  584. }
  585. protected function getExceptionConstructor($explanation, $arguments = array())
  586. {
  587. $trace = debug_backtrace();
  588. $methodCount = 0;
  589. $ignoredClasses = array(get_class($this), "Nunzion\\Expect");
  590. while (isset($trace[$methodCount]["class"])
  591. && in_array($trace[$methodCount]["class"], $ignoredClasses)) {
  592. $methodCount++;
  593. }
  594. $methodCount--;
  595. $callerStackFrame = $trace[$methodCount];
  596. $callersCallerStackFrame = null;
  597. if (isset($trace[$methodCount + 1]))
  598. $callersCallerStackFrame = $trace[$methodCount + 1];
  599. $expression = $this->getThatParameterExpression($callerStackFrame) . $this->path;
  600. $arguments["path"] = $this->path;
  601. $arguments["expression"] = $expression;
  602. $matches = array();
  603. preg_match("/\\$(?<variableName>[a-zA-Z_][a-zA-Z0-9_]*)/", $expression, $matches);
  604. if (count($matches) > 0)
  605. {
  606. $pathParts = explode("->", str_replace("[", "->", ltrim($expression, "$")));
  607. $variableName = $matches["variableName"];
  608. if ($variableName === "this")
  609. {
  610. //e.g. Member $this->foo is invalid: $this->foo->bar cannot be smaller than 3.
  611. //e.g. Member $this->foo cannot be smaller than 3.
  612. $arguments["member"] = $pathParts[1];
  613. if (count($pathParts) > 2)
  614. $message = "Member '{member}' is invalid: '{expression}' " . $explanation;
  615. else
  616. $message = "Member '{member}' " . $explanation;
  617. return $this->getInvariantViolationExceptionConstructor(
  618. $this->format($message . ".", $arguments), $arguments);
  619. }
  620. else if ($callersCallerStackFrame !== null
  621. && $this->isParameter($variableName, $callersCallerStackFrame))
  622. {
  623. $arguments["parameter"] = $pathParts[0];
  624. if (count($pathParts) > 1)
  625. $message = "Parameter '{parameter}' is invalid: '{expression}' " . $explanation;
  626. else
  627. $message = "Parameter '{parameter}' " . $explanation;
  628. return $this->getPreconditionViolationExceptionConstructor(
  629. $this->format($message . ".", $arguments), $arguments);
  630. }
  631. else
  632. {
  633. return $this->getConditionViolationExceptionConstructor(
  634. $this->format("'{expression}' " . $explanation . ".", $arguments), $arguments);
  635. }
  636. }
  637. return $this->getConditionViolationExceptionConstructor(
  638. $this->format("The value " . $explanation . ".", $arguments), $arguments);
  639. }
  640. protected function getUnexpectedValueExceptionConstructor($explanation, $arguments = array())
  641. {
  642. $arguments["actual"] = $this->value;
  643. return $this->getExceptionConstructor($explanation . ", but is {actual}", $arguments);
  644. }
  645. protected function getTypeMismatchExceptionConstructor($expectedType, $message = null)
  646. {
  647. if (!$this->isDefined)
  648. $typeStr = $type = "undefined";
  649. else if ($this->value === null)
  650. $typeStr = $type = "null";
  651. else
  652. $typeStr = "type of '" . ($type = is_object($this->value) ? get_class($this->value) : gettype($this->value)) . "'";
  653. if ($message === null)
  654. $message = "must be type of '{expected}', but is {actualText}";
  655. return $this->getExceptionConstructor($message,
  656. array("expected" => $expectedType,
  657. "actual" => $type,
  658. "actualText" => $typeStr));
  659. }
  660. // </editor-fold>
  661. }