Factory.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. <?php
  2. /*
  3. * Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
  4. * SPDX-License-Identifier: MIT
  5. */
  6. declare(strict_types=1);
  7. namespace Respect\Validation;
  8. use ReflectionClass;
  9. use ReflectionException;
  10. use ReflectionObject;
  11. use Respect\Validation\Exceptions\ComponentException;
  12. use Respect\Validation\Exceptions\InvalidClassException;
  13. use Respect\Validation\Exceptions\ValidationException;
  14. use Respect\Validation\Message\Formatter;
  15. use Respect\Validation\Message\ParameterStringifier;
  16. use Respect\Validation\Message\Stringifier\KeepOriginalStringName;
  17. use function array_merge;
  18. use function lcfirst;
  19. use function sprintf;
  20. use function str_replace;
  21. use function trim;
  22. use function ucfirst;
  23. /**
  24. * Factory of objects.
  25. *
  26. * @author Augusto Pascutti <augusto@phpsp.org.br>
  27. * @author Henrique Moody <henriquemoody@gmail.com>
  28. */
  29. final class Factory
  30. {
  31. /**
  32. * @var string[]
  33. */
  34. private $rulesNamespaces = ['Respect\\Validation\\Rules'];
  35. /**
  36. * @var string[]
  37. */
  38. private $exceptionsNamespaces = ['Respect\\Validation\\Exceptions'];
  39. /**
  40. * @var callable
  41. */
  42. private $translator = 'strval';
  43. /**
  44. * @var ParameterStringifier
  45. */
  46. private $parameterStringifier;
  47. /**
  48. * Default instance of the Factory.
  49. *
  50. * @var Factory
  51. */
  52. private static $defaultInstance;
  53. public function __construct()
  54. {
  55. $this->parameterStringifier = new KeepOriginalStringName();
  56. }
  57. /**
  58. * Returns the default instance of the Factory.
  59. */
  60. public static function getDefaultInstance(): self
  61. {
  62. if (self::$defaultInstance === null) {
  63. self::$defaultInstance = new self();
  64. }
  65. return self::$defaultInstance;
  66. }
  67. public function withRuleNamespace(string $rulesNamespace): self
  68. {
  69. $clone = clone $this;
  70. $clone->rulesNamespaces[] = trim($rulesNamespace, '\\');
  71. return $clone;
  72. }
  73. public function withExceptionNamespace(string $exceptionsNamespace): self
  74. {
  75. $clone = clone $this;
  76. $clone->exceptionsNamespaces[] = trim($exceptionsNamespace, '\\');
  77. return $clone;
  78. }
  79. public function withTranslator(callable $translator): self
  80. {
  81. $clone = clone $this;
  82. $clone->translator = $translator;
  83. return $clone;
  84. }
  85. public function withParameterStringifier(ParameterStringifier $parameterStringifier): self
  86. {
  87. $clone = clone $this;
  88. $clone->parameterStringifier = $parameterStringifier;
  89. return $clone;
  90. }
  91. /**
  92. * Creates a rule.
  93. *
  94. * @param mixed[] $arguments
  95. *
  96. * @throws ComponentException
  97. */
  98. public function rule(string $ruleName, array $arguments = []): Validatable
  99. {
  100. foreach ($this->rulesNamespaces as $namespace) {
  101. try {
  102. /** @var class-string<Validatable> $name */
  103. $name = $namespace . '\\' . ucfirst($ruleName);
  104. /** @var Validatable $rule */
  105. $rule = $this
  106. ->createReflectionClass($name, Validatable::class)
  107. ->newInstanceArgs($arguments);
  108. return $rule;
  109. } catch (ReflectionException $exception) {
  110. continue;
  111. }
  112. }
  113. throw new ComponentException(sprintf('"%s" is not a valid rule name', $ruleName));
  114. }
  115. /**
  116. * Creates an exception.
  117. *
  118. * @param mixed $input
  119. * @param mixed[] $extraParams
  120. *
  121. * @throws ComponentException
  122. */
  123. public function exception(Validatable $validatable, $input, array $extraParams = []): ValidationException
  124. {
  125. $formatter = new Formatter($this->translator, $this->parameterStringifier);
  126. $reflection = new ReflectionObject($validatable);
  127. $ruleName = $reflection->getShortName();
  128. $params = ['input' => $input] + $extraParams + $this->extractPropertiesValues($validatable, $reflection);
  129. $id = lcfirst($ruleName);
  130. if ($validatable->getName() !== null) {
  131. /*$id = */$params['name'] = $validatable->getName();
  132. }
  133. $exceptionNamespace = str_replace('\\Rules', '\\Exceptions', $reflection->getNamespaceName());
  134. foreach (array_merge([$exceptionNamespace], $this->exceptionsNamespaces) as $namespace) {
  135. try {
  136. /** @var class-string<ValidationException> $exceptionName */
  137. $exceptionName = $namespace . '\\' . $ruleName . 'Exception';
  138. return $this->createValidationException(
  139. $exceptionName,
  140. $id,
  141. $input,
  142. $params,
  143. $formatter
  144. );
  145. } catch (ReflectionException $exception) {
  146. continue;
  147. }
  148. }
  149. return new ValidationException($input, $id, $params, $formatter);
  150. }
  151. /**
  152. * Define the default instance of the Factory.
  153. */
  154. public static function setDefaultInstance(self $defaultInstance): void
  155. {
  156. self::$defaultInstance = $defaultInstance;
  157. }
  158. /**
  159. * Creates a reflection based on class name.
  160. *
  161. * @param class-string $name
  162. * @param class-string $parentName
  163. *
  164. * @throws InvalidClassException
  165. * @throws ReflectionException
  166. *
  167. * @return ReflectionClass<ValidationException|Validatable|object>
  168. */
  169. private function createReflectionClass(string $name, string $parentName): ReflectionClass
  170. {
  171. $reflection = new ReflectionClass($name);
  172. if (!$reflection->isSubclassOf($parentName) && $parentName !== $name) {
  173. throw new InvalidClassException(sprintf('"%s" must be an instance of "%s"', $name, $parentName));
  174. }
  175. if (!$reflection->isInstantiable()) {
  176. throw new InvalidClassException(sprintf('"%s" must be instantiable', $name));
  177. }
  178. return $reflection;
  179. }
  180. /**
  181. * Creates a Validation exception.
  182. *
  183. * @param class-string<ValidationException> $exceptionName
  184. *
  185. * @param mixed $input
  186. * @param mixed[] $params
  187. *
  188. * @throws InvalidClassException
  189. * @throws ReflectionException
  190. */
  191. private function createValidationException(
  192. string $exceptionName,
  193. string $id,
  194. $input,
  195. array $params,
  196. Formatter $formatter
  197. ): ValidationException {
  198. /** @var ValidationException $exception */
  199. $exception = $this
  200. ->createReflectionClass($exceptionName, ValidationException::class)
  201. ->newInstance($input, $id, $params, $formatter);
  202. if (isset($params['template'])) {
  203. $exception->updateTemplate($params['template']);
  204. }
  205. return $exception;
  206. }
  207. /**
  208. * @param ReflectionObject|ReflectionClass<Validatable> $reflection
  209. * @return mixed[]
  210. */
  211. private function extractPropertiesValues(Validatable $validatable, ReflectionClass $reflection): array
  212. {
  213. $values = [];
  214. foreach ($reflection->getProperties() as $property) {
  215. $property->setAccessible(true);
  216. $propertyValue = $property->getValue($validatable);
  217. if ($propertyValue === null) {
  218. continue;
  219. }
  220. $values[$property->getName()] = $propertyValue;
  221. }
  222. $parentReflection = $reflection->getParentClass();
  223. if ($parentReflection !== false) {
  224. return $values + $this->extractPropertiesValues($validatable, $parentReflection);
  225. }
  226. return $values;
  227. }
  228. }