ReflectionService.php (14119B)
1 <?php 2 3 namespace PartKeepr\DoctrineReflectionBundle\Services; 4 5 use Doctrine\Bundle\DoctrineBundle\Registry; 6 use Doctrine\Common\Annotations\Reader; 7 use Doctrine\ORM\EntityManager; 8 use Doctrine\ORM\Mapping\ClassMetadata; 9 use Doctrine\ORM\Mapping\ClassMetadataInfo; 10 use Symfony\Component\Templating\EngineInterface; 11 use Symfony\Component\Validator\Constraint; 12 use Symfony\Component\Validator\Constraints\NotBlank; 13 14 class ReflectionService 15 { 16 /** @var EntityManager */ 17 protected $em; 18 19 protected $templateEngine; 20 21 protected $reader; 22 23 public function __construct(Registry $doctrine, EngineInterface $templateEngine, Reader $reader) 24 { 25 $this->templateEngine = $templateEngine; 26 $this->em = $doctrine->getManager(); 27 $this->reader = $reader; 28 } 29 30 /** 31 * Returns a list of all registered entities, converted to the ExtJS naming scheme (. instead of \). 32 * 33 * @return array 34 */ 35 public function getEntities() 36 { 37 $entities = []; 38 39 $meta = $this->em->getMetadataFactory()->getAllMetadata(); 40 41 foreach ($meta as $m) { 42 /* @var ClassMetadata $m */ 43 $entities[] = $this->convertPHPToExtJSClassName($m->getName()); 44 } 45 46 return $entities; 47 } 48 49 /** 50 * Returns the ExtJS Model contents for a given entity. 51 * 52 * @param $entity string The ExtJS class name 53 * 54 * @return string The ExtJS model code 55 */ 56 public function getEntity($entity) 57 { 58 $bTree = false; 59 60 $parentClass = 'PartKeepr.data.HydraModel'; 61 62 $entity = $this->convertExtJSToPHPClassName($entity); 63 64 $cm = $this->em->getClassMetadata($entity); 65 66 if ($cm->getReflectionClass()->isSubclassOf("PartKeepr\CategoryBundle\Entity\AbstractCategory")) { 67 $parentClass = 'PartKeepr.data.HydraTreeModel'; 68 $bTree = true; 69 } 70 71 $fieldMappings = []; 72 73 $fieldMappings = array_merge($fieldMappings, $this->getVirtualFieldMappings($cm)); 74 $fieldMappings = array_merge($fieldMappings, $this->getDatabaseFieldMappings($cm)); 75 76 $associationMappings = $this->getDatabaseAssociationMappings($cm, $bTree); 77 78 $associationMappings["ONE_TO_MANY"] = array_merge( 79 $associationMappings["ONE_TO_MANY"], 80 $this->getVirtualOneToManyRelationMappings($cm) 81 ); 82 83 $renderParams = [ 84 'fields' => $fieldMappings, 85 'associations' => $associationMappings, 86 'className' => $this->convertPHPToExtJSClassName($entity), 87 'parentClass' => $parentClass, 88 ]; 89 90 $targetService = $this->reader->getClassAnnotation( 91 $cm->getReflectionClass(), 92 "PartKeepr\DoctrineReflectionBundle\Annotation\TargetService" 93 ); 94 95 if ($targetService !== null) { 96 $renderParams['uri'] = $targetService->uri; 97 } 98 99 $ignoreIds = $this->reader->getClassAnnotation( 100 $cm->getReflectionClass(), 101 "PartKeepr\DoctrineReflectionBundle\Annotation\IgnoreIds" 102 ); 103 104 if ($ignoreIds !== null) { 105 $renderParams['ignoreIds'] = true; 106 } else { 107 $renderParams['ignoreIds'] = false; 108 } 109 110 return $this->templateEngine->render('PartKeeprDoctrineReflectionBundle::model.js.twig', $renderParams); 111 } 112 113 /** 114 * Returns association mapping for a given entity. 115 * 116 * @param ClassMetadata $cm 117 * @param bool|false $bTree 118 * 119 * @return array 120 */ 121 protected function getDatabaseAssociationMappings(ClassMetadata $cm, $bTree = false) 122 { 123 $associations = $cm->getAssociationMappings(); 124 $byReferenceMappings = $this->getByReferenceMappings($cm); 125 126 $associationMappings = [ 127 "ONE_TO_ONE" => [], 128 "MANY_TO_ONE" => [], 129 "ONE_TO_MANY" => [], 130 "MANY_TO_MANY" => [], 131 ]; 132 133 foreach ($associations as $association) { 134 $getterPlural = false; 135 $associationType = $association['type']; 136 137 switch ($association['type']) { 138 case ClassMetadataInfo::MANY_TO_MANY: 139 $associationType = 'MANY_TO_MANY'; 140 $getterPlural = true; 141 break; 142 case ClassMetadataInfo::MANY_TO_ONE: 143 $associationType = 'MANY_TO_ONE'; 144 $getterPlural = false; 145 break; 146 case ClassMetadataInfo::ONE_TO_MANY: 147 $associationType = 'ONE_TO_MANY'; 148 $getterPlural = true; 149 break; 150 case ClassMetadataInfo::ONE_TO_ONE: 151 $associationType = 'ONE_TO_ONE'; 152 $getterPlural = false; 153 break; 154 } 155 156 $getter = 'get'.ucfirst($association['fieldName']); 157 $getterField = lcfirst($cm->getReflectionClass()->getShortName()).str_replace( 158 '.', 159 '', 160 $this->convertPHPToExtJSClassName($association['targetEntity']) 161 ); 162 163 if ($getterPlural) { 164 $getterField .= 's'; 165 } 166 167 $propertyAnnotations = $this->reader->getPropertyAnnotations( 168 $cm->getReflectionProperty($association['fieldName']) 169 ); 170 171 $nullable = true; 172 173 foreach ($propertyAnnotations as $propertyAnnotation) { 174 $filter = "Symfony\\Component\\Validator\\Constraints\\NotNull"; 175 176 if (substr(get_class($propertyAnnotation), 0, strlen($filter)) === $filter) { 177 $nullable = false; 178 } 179 } 180 181 // The self-referencing association may not be written for trees, because ExtJS can't load all nodes 182 // in one go. 183 if (!($bTree && $association['targetEntity'] == $cm->getName())) { 184 $byReference = false; 185 186 if (in_array($association['fieldName'], $byReferenceMappings)) { 187 $byReference = true; 188 } 189 $associationMappings[$associationType][] = [ 190 'name' => $association['fieldName'], 191 'nullable' => $nullable, 192 'target' => $this->convertPHPToExtJSClassName($association['targetEntity']), 193 'byReference' => $byReference, 194 'getter' => $getter, 195 'getterField' => $getterField, 196 ]; 197 } 198 } 199 200 return $associationMappings; 201 } 202 203 /** 204 * Returns all virtual field mappings. 205 * 206 * @param ClassMetadata $cm 207 * 208 * @return array 209 */ 210 protected function getVirtualFieldMappings(ClassMetadata $cm) 211 { 212 $fieldMappings = []; 213 214 foreach ($cm->getReflectionClass()->getProperties() as $property) { 215 $virtualFieldAnnotation = $this->reader->getPropertyAnnotation( 216 $property, 217 'PartKeepr\DoctrineReflectionBundle\Annotation\VirtualField' 218 ); 219 220 if ($virtualFieldAnnotation !== null) { 221 $fieldMappings[] = [ 222 'persist' => true, 223 'name' => $property->getName(), 224 'type' => $this->getExtJSFieldMapping($virtualFieldAnnotation->type), 225 ]; 226 } 227 } 228 229 return $fieldMappings; 230 } 231 232 /** 233 * Returns all virtual relations mappings. 234 * 235 * @param ClassMetadata $cm 236 * 237 * @return array 238 */ 239 protected function getVirtualOneToManyRelationMappings(ClassMetadata $cm) 240 { 241 $virtualRelationMappings = []; 242 243 foreach ($cm->getReflectionClass()->getProperties() as $property) { 244 $virtualOneToManyRelation = $this->reader->getPropertyAnnotation( 245 $property, 246 'PartKeepr\DoctrineReflectionBundle\Annotation\VirtualOneToMany' 247 ); 248 249 if ($virtualOneToManyRelation !== null) { 250 $virtualRelationMappings[] = 251 [ 252 'name' => $property->getName(), 253 'target' => $this->convertPHPToExtJSClassName($virtualOneToManyRelation->target), 254 ]; 255 } 256 } 257 258 return $virtualRelationMappings; 259 } 260 261 /** 262 * Returns all by-reference associations. 263 * 264 * @param ClassMetadata $cm 265 * 266 * @return array 267 */ 268 protected function getByReferenceMappings(ClassMetadata $cm) 269 { 270 $byReferenceMappings = []; 271 272 foreach ($cm->getReflectionClass()->getProperties() as $property) { 273 $byReferenceAnnotation = $this->reader->getPropertyAnnotation( 274 $property, 275 'PartKeepr\DoctrineReflectionBundle\Annotation\ByReference' 276 ); 277 278 if ($byReferenceAnnotation !== null) { 279 $byReferenceMappings[] = $property->getName(); 280 } 281 } 282 283 return $byReferenceMappings; 284 } 285 286 /** 287 * Returns database field mappings. 288 * 289 * @param ClassMetadata $cm 290 * 291 * @throws \Doctrine\ORM\Mapping\MappingException 292 * 293 * @return array 294 */ 295 protected function getDatabaseFieldMappings(ClassMetadata $cm) 296 { 297 $fieldMappings = []; 298 $fields = $cm->getFieldNames(); 299 300 foreach ($fields as $field) { 301 $currentMapping = $cm->getFieldMapping($field); 302 303 $asserts = $this->getExtJSAssertMappings($cm, $field); 304 305 if ($currentMapping['fieldName'] == 'id') { 306 $currentMapping['fieldName'] = '@id'; 307 $currentMapping['type'] = 'string'; 308 } 309 310 if (!array_key_exists("nullable", $currentMapping)) { 311 $currentMapping["nullable"] = false; 312 } 313 314 $fieldMappings[] = [ 315 'name' => $currentMapping['fieldName'], 316 'type' => $this->getExtJSFieldMapping($currentMapping['type']), 317 'nullable' => $currentMapping['nullable'], 318 'validators' => json_encode($asserts), 319 'persist' => $this->allowPersist($cm, $field), 320 ]; 321 } 322 323 return $fieldMappings; 324 } 325 326 /** 327 * Converts a doctrine/PHP type to the ExtJS type. 328 * 329 * @param $type string the PHP/doctrine type 330 * 331 * @return string The ExtJS type 332 */ 333 protected function getExtJSFieldMapping($type) 334 { 335 switch ($type) { 336 case 'integer': 337 return 'int'; 338 break; 339 case 'string': 340 return 'string'; 341 break; 342 case 'text': 343 return 'string'; 344 break; 345 case 'datetime': 346 return 'date'; 347 break; 348 case 'boolean': 349 return 'boolean'; 350 break; 351 case 'float': 352 return 'number'; 353 break; 354 case 'decimal': 355 return 'number'; 356 break; 357 case 'array': 358 return 'array'; 359 break; 360 } 361 362 return 'undefined'; 363 } 364 365 public function getExtJSAssertMapping(Constraint $assert) 366 { 367 switch (get_class($assert)) { 368 case "Symfony\\Component\\Validator\\Constraints\\NotBlank": 369 /** 370 * @var NotBlank 371 */ 372 return ["type" => "presence", "message" => $assert->message]; 373 break; 374 default: 375 return false; 376 } 377 } 378 379 public function getExtJSAssertMappings(ClassMetadata $cm, $field) 380 { 381 $asserts = []; 382 $propertyAnnotations = $this->reader->getPropertyAnnotations($cm->getReflectionProperty($field)); 383 384 foreach ($propertyAnnotations as $propertyAnnotation) { 385 $filter = "Symfony\\Component\\Validator\\Constraints\\"; 386 387 if (substr(get_class($propertyAnnotation), 0, strlen($filter)) === $filter) { 388 $assertMapping = $this->getExtJSAssertMapping($propertyAnnotation); 389 390 if ($assertMapping !== false) { 391 $asserts[] = $assertMapping; 392 } 393 } 394 } 395 396 return $asserts; 397 } 398 399 public function allowPersist(ClassMetadata $cm, $field) 400 { 401 $groupsAnnotation = $this->reader->getPropertyAnnotation( 402 $cm->getReflectionProperty($field), 403 'Symfony\Component\Serializer\Annotation\Groups' 404 ); 405 406 if ($groupsAnnotation !== null) { 407 if (in_array("readonly", $groupsAnnotation->getGroups())) { 408 return false; 409 } 410 } 411 412 return true; 413 } 414 415 /** 416 * Converts a PHP class name with namespaces to an ExtJS class name with namespaces. 417 * 418 * @param $className 419 * 420 * @return string 421 */ 422 public function convertPHPToExtJSClassName($className) 423 { 424 return str_replace('\\', '.', $className); 425 } 426 427 /** 428 * Converts an ExtJS class name with namespaces to a PHP class name with namespaces. 429 * 430 * @param $className 431 * 432 * @return string 433 */ 434 public function convertExtJSToPHPClassName($className) 435 { 436 return str_replace('.', '\\', $className); 437 } 438 439 public function createCache($cacheDir) 440 { 441 @mkdir($cacheDir, 0777, true); 442 443 $entities = $this->getEntities(); 444 445 foreach ($entities as $entity) { 446 $model = $this->getEntity($entity); 447 448 $this->writeCacheFile($cacheDir.'/'.$entity.'.js', $model); 449 } 450 } 451 452 protected function writeCacheFile($file, $content) 453 { 454 $tmpFile = tempnam(dirname($file), basename($file)); 455 if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $file)) { 456 @chmod($file, 0666 & ~umask()); 457 458 return; 459 } 460 461 throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $file)); 462 } 463 }