partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

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 }