DefinitionGenerator.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. <?php
  2. namespace PhpDevCommunity\RequestKit\Generator;
  3. use PhpDevCommunity\RequestKit\Builder\SchemaObjectFactory;
  4. use PhpDevCommunity\RequestKit\Type;
  5. use ReflectionClass;
  6. use ReflectionException;
  7. use ReflectionProperty;
  8. final class DefinitionGenerator
  9. {
  10. private SchemaObjectFactory $factory;
  11. public function __construct(SchemaObjectFactory $factory)
  12. {
  13. $this->factory = $factory;
  14. }
  15. /**
  16. * @param string|object $objectClass
  17. * @return array
  18. */
  19. public function generateFromObject($objectClass): array
  20. {
  21. if (is_object($objectClass)) {
  22. $object = $objectClass;
  23. }else {
  24. $object = new $objectClass();
  25. }
  26. $metadata = $this->cacheGet($object);
  27. if (!empty($metadata)) {
  28. return $this->generateFromMetadata($object, $metadata);
  29. }
  30. $reflection = new ReflectionClass($object);
  31. $metadata['object_class'] = get_class($object);
  32. $metadata['php_class'] = $reflection->getExtensionName() === false;
  33. $metadata['properties'] = [];
  34. foreach ($reflection->getProperties() as $property) {
  35. $type = $property->getType();
  36. $propertyName = self::camelCaseToSnakeCase($property->getName());
  37. $phpDoc = $property->getDocComment();
  38. $example = self::parsePhpDocTag($phpDoc, 'example')[0] ?? null;
  39. $required = false;
  40. if ($type) {
  41. $name = $type->getName();
  42. $propertyType = $name;
  43. if (in_array($name, ['array', 'iterable'], true)) {
  44. $arrayType = self::extractArrayType(self::parsePhpDocTag($phpDoc, 'var')[0] ?? '', $property);
  45. $propertyType = class_exists($arrayType) ? "array_of_item:$arrayType" : "array_of_$arrayType";
  46. }
  47. if (!$type->allowsNull() && !str_starts_with($propertyType, 'array_of_')) {
  48. $required = true;
  49. }
  50. } else {
  51. $propertyType = 'string';
  52. }
  53. $metadata['properties'][$propertyName]['type'] = $propertyType;
  54. $metadata['properties'][$propertyName]['public'] = $property->isPublic();
  55. $metadata['properties'][$propertyName]['name'] = $property->getName();
  56. $metadata['properties'][$propertyName]['required'] = $required;
  57. $metadata['properties'][$propertyName]['example'] = $example;
  58. }
  59. $this->cacheSet($object, $metadata);
  60. return $this->generateFromMetadata($object, $metadata);
  61. }
  62. private function generateFromMetadata(object $object, array $metadata): array
  63. {
  64. $definitions = [];
  65. foreach ($metadata['properties'] as $name => $property) {
  66. $type = $property['type'];
  67. $example = $property['example'];
  68. $propertyName = $property['name'];
  69. $required = $property['required'];
  70. $defaultValue = null;
  71. if ($property['public'] && isset($object->$propertyName)) {
  72. $defaultValue = $object->$propertyName;
  73. } elseif (method_exists($object, 'get' . ucfirst($propertyName))) {
  74. $defaultValue = $object->{'get' . ucfirst($propertyName)}();
  75. }elseif (method_exists($object, 'is' . ucfirst($propertyName))) {
  76. $defaultValue = $object->{'is' . ucfirst($propertyName)}();
  77. }
  78. if (str_starts_with( $type, 'array_of_item:')) {
  79. $class = substr($type, 14);
  80. $definitionType = Type::typeObject($class);
  81. if ($definitionType === null) {
  82. $definitionType = new Type\ItemType($this->factory->createSchemaFromObject($class));
  83. }
  84. $definition = Type::arrayOf($definitionType);
  85. }elseif (class_exists($type)) {
  86. $definition = Type::typeObject($type);
  87. if ($definition === null) {
  88. $definition = new Type\ItemType($this->factory->createSchemaFromObject($type));
  89. }
  90. } else {
  91. $definition = Type::type($type);
  92. }
  93. $definition->example($example);
  94. $definition->default($defaultValue);
  95. if ($required) {
  96. $definition->required();
  97. }
  98. $definitions[$name] = $definition;
  99. }
  100. return $definitions;
  101. }
  102. private function cacheGet(object $object)
  103. {
  104. $key = md5(get_class($object));
  105. if ($this->factory->getCacheDir()) {
  106. $file = $this->factory->getCacheDir() . DIRECTORY_SEPARATOR . $key . '.definition.json';
  107. if (file_exists($file)) {
  108. return unserialize(file_get_contents($file));
  109. }
  110. }
  111. return [];
  112. }
  113. private function cacheSet(object $object, array $metadata): void
  114. {
  115. $key = md5(get_class($object));
  116. if ($this->factory->getCacheDir()) {
  117. $file = $this->factory->getCacheDir() . DIRECTORY_SEPARATOR . $key . '.definition.json';
  118. file_put_contents($file, serialize($metadata));
  119. }
  120. }
  121. private static function camelCaseToSnakeCase(string $camelCaseString): string
  122. {
  123. return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $camelCaseString));
  124. }
  125. private static function parsePhpDocTag($phpDoc, string $tag): array
  126. {
  127. if (!is_string($phpDoc) || empty($phpDoc)) {
  128. return [];
  129. }
  130. $matches = [];
  131. $pattern = '/\*\s*@' . preg_quote($tag, '/') . '\s+([^\n]+)/';
  132. preg_match_all($pattern, $phpDoc, $matches);
  133. return $matches[1] ?? [];
  134. }
  135. private static function extractArrayType(string $type, ReflectionProperty $property): ?string
  136. {
  137. if (preg_match('/array<([^>]+)>/', $type, $matches)) {
  138. $typeParsed = trim($matches[1]);
  139. if (self::isNativeType($typeParsed)) {
  140. return $typeParsed;
  141. }
  142. $classname = $typeParsed;
  143. if (class_exists($classname)) {
  144. return $classname;
  145. }
  146. $declaringClass = $property->getDeclaringClass();
  147. $namespace = $declaringClass->getNamespaceName();
  148. $fullClassName = $namespace ? "$namespace\\$classname" : $classname;
  149. return class_exists($fullClassName) ? $fullClassName : null;
  150. }
  151. return null;
  152. }
  153. private static function isNativeType(string $type): bool
  154. {
  155. $nativeTypes = [
  156. 'int', 'integer',
  157. 'float', 'double',
  158. 'string',
  159. 'bool', 'boolean',
  160. 'array',
  161. 'object',
  162. 'callable',
  163. 'iterable',
  164. 'resource',
  165. 'null',
  166. ];
  167. return in_array($type, $nativeTypes, true);
  168. }
  169. }