src/TypesGenerator.php
public function generate(array $config): void
    {
        $baseClass = [
            'constants' => [],
            'fields' => [],
            'uses' => [],
            'hasConstructor' => false,
            'parentHasConstructor' => false,
            'hasChild' => false,
            'abstract' => false,
        ];

        $typesToGenerate = [];

        if (!$config['types']) {
            foreach ($this->graphs as $graph) {
                $typesToGenerate = $graph->allOfType('rdfs:Class');
            }
        } else {
            foreach ($config['types'] as $typeName => $typeConfig) {
                $resource = null;
                foreach ($this->graphs as $graph) {
                    $resources = $graph->resources();

                    if (isset($resources[$typeConfig['vocabularyNamespace'].$typeName])) {
                        $resource = $graph->resource($typeConfig['vocabularyNamespace'].$typeName, 'rdfs:Class');
                        break;
                    }
                }

                if ($resource) {
                    $typesToGenerate[$typeName] = $resource;
                } elseif (empty($graph)) {
                    $this->logger->warning('Type "{typeName}" cannot be found and there is no graph to generate entity.');
                } else {
                    $this->logger->warning('Type "{typeName}" cannot be found. Using "{guessFrom}" type to generate entity.', ['typeName' => $typeName, 'guessFrom' => $typeConfig['guessFrom']]);
                    $type = $graph->resource($typeConfig['vocabularyNamespace'].$typeConfig['guessFrom'], 'rdfs:Class');
                    $typesToGenerate[$typeName] = $type;
                }
            }
        }

        $classes = [];
        $propertiesMap = $this->createPropertiesMap($typesToGenerate);

        foreach ($typesToGenerate as $typeName => $type) {
            $typeName = is_string($typeName) ? $typeName : $type->localName();
            $typeConfig = $config['types'][$typeName] ?? null;
            $class = $baseClass;

            $comment = $type->get('rdfs:comment');

            $class['name'] = $typeName;
            $class['label'] = $comment ? $comment->getValue() : '';
            $class['resource'] = $type;
            $class['config'] = $typeConfig;

            $class['isEnum'] = $this->isEnum($type);
            if ($class['isEnum']) {
                $class['namespace'] = $typeConfig['namespace'] ?? $config['namespaces']['enum'];
                $class['parent'] = 'Enum';
                $class['uses'][] = Enum::class;

                // Constants
                foreach ($this->graphs as $graph) {
                    foreach ($graph->allOfType($type->getUri()) as $instance) {
                        $class['constants'][$instance->localName()] = [
                            'name' => strtoupper(substr(preg_replace('/([A-Z])/', '_$1', $instance->localName()), 1)),
                            'resource' => $instance,
                            'value' => $instance->getUri(),
                        ];
                    }
                }
            } else {
                // Entities
                $class['namespace'] = $typeConfig['namespaces']['class'] ?? $config['namespaces']['entity'];

                // Parent
                $class['parent'] = $typeConfig['parent'] ?? null;
                if (null === $class['parent']) {
                    $numberOfSupertypes = count($type->all('rdfs:subClassOf'));

                    if ($numberOfSupertypes > 1) {
                        $this->logger->warning(sprintf('The type "%s" has several supertypes. Using the first one.', $type->localName()));
                    }

                    $class['parent'] = $numberOfSupertypes ? $type->all('rdfs:subClassOf')[0]->localName() : false;
                }

                if (isset($class['parent']) && isset($config['types'][$class['parent']]['namespaces']['class'])) {
                    $parentNamespace = $config['types'][$class['parent']]['namespaces']['class'];

                    if ($parentNamespace !== $class['namespace']) {
                        $class['uses'][] = $parentNamespace.'\\'.$class['parent'];
                    }
                }

                // Embeddable
                $class['embeddable'] = $typeConfig['embeddable'] ?? false;

                if (!$config['types'] && $class['parent'] && !isset($config['types'][$class['parent']])) {
                    $this->logger->error(sprintf('The type "%s" (parent of "%s") doesn\'t exist', $class['parent'], $type->localName()));
                }

                // Interfaces
                if ($config['useInterface']) {
                    $class['interfaceNamespace'] = isset($typeConfig['namespaces']['interface']) && $typeConfig['namespaces']['interface'] ? $typeConfig['namespaces']['interface'] : $config['namespaces']['interface'];
                    $class['interfaceName'] = sprintf('%sInterface', $typeName);
                }
            }

            // Fields
            if (!$typeConfig['allProperties'] && isset($typeConfig['properties']) && is_array($typeConfig['properties'])) {
                foreach ($typeConfig['properties'] as $key => $value) {
                    foreach ($propertiesMap[$type->getUri()] as $property) {
                        if ($key !== $property->localName()) {
                            continue;
                        }

                        $class = $this->generateField($config, $class, $type, $typeName, $property->localName(), $property);
                        continue 2;
                    }

                    // Add custom fields (non schema.org)
                    $this->logger->info(sprintf('The property "%s" (type "%s") is a custom property.', $key, $type->localName()));
                    $class = $this->generateField($config, $class, $type, $typeName, $key);
                }
            } else {
                // All properties
                foreach ($propertiesMap[$type->getUri()] as $property) {
                    $class = $this->generateField($config, $class, $type, $typeName, $property->localName(), $property);
                }
            }

            $classes[$typeName] = $class;
        }

        // Second pass
        foreach ($classes as &$class) {
            if ($class['parent'] && isset($classes[$class['parent']])) {
                $classes[$class['parent']]['hasChild'] = true;
                $class['parentHasConstructor'] = $classes[$class['parent']]['hasConstructor'];
            }

            foreach ($class['fields'] as &$field) {
                $field['isEnum'] = isset($classes[$field['range']]) && $classes[$field['range']]['isEnum'];
                $field['typeHint'] = $this->fieldToTypeHint($config, $field, $classes) ?? false;

                if ($field['isArray']) {
                    $field['adderRemoverTypeHint'] = $this->fieldToAdderRemoverTypeHint($field, $classes) ?? false;
                }
            }
        }

        // Third pass
        foreach ($classes as &$class) {
            $class['abstract'] = $config['types'][$class['name']]['abstract'] ?? $class['hasChild'];

            // When including all properties, ignore properties already set on parent
            if (isset($config['types'][$class['name']]['allProperties']) && $config['types'][$class['name']]['allProperties'] && isset($classes[$class['parent']])) {
                $type = $class['resource'];

                foreach ($propertiesMap[$type->getUri()] as $property) {
                    if (!isset($class['fields'][$property->localName()])) {
                        continue;
                    }

                    $parentConfig = $config['types'][$class['parent']] ?? null;
                    $parentClass = $classes[$class['parent']];

                    while ($parentClass) {
                        if (!isset($parentConfig['properties']) || !is_array($parentConfig['properties'])) {
                            // Unset implicit property
                            $parentType = $parentClass['resource'];
                            if (in_array($property, $propertiesMap[$parentType->getUri()], true)) {
                                unset($class['fields'][$property->localName()]);
                                continue 2;
                            }
                        } else {
                            // Unset explicit property
                            if (array_key_exists($property->localName(), $parentConfig['properties'])) {
                                unset($class['fields'][$property->localName()]);
                                continue 2;
                            }
                        }

                        $parentConfig = $parentClass['parent'] ? ($config['types'][$parentClass['parent']] ?? null) : null;
                        $parentClass = $parentClass['parent'] ? $classes[$parentClass['parent']] : null;
                    }
                }
            }
        }

        // Generate ID
        if ($config['id']['generate']) {
            foreach ($classes as &$class) {
                if ($class['hasChild'] || $class['isEnum'] || $class['embeddable']) {
                    continue;
                }

                switch ($config['id']['generationStrategy']) {
                    case 'auto':
                        $range = 'Integer';
                        $typeHint = 'int';
                        $writable = false;
                        $nullable = true;
                        break;
                    case 'uuid':
                        $range = 'Text';
                        $typeHint = 'string';
                        $writable = $config['id']['writable'];
                        $nullable = !$writable;
                        break;
                    case 'mongoid':
                        $range = 'Text';
                        $typeHint = 'string';
                        $writable = false;
                        $nullable = true;
                        break;
                    default:
                        $range = 'Text';
                        $typeHint = 'string';
                        $writable = true;
                        $nullable = false;
                        break;
                }

                $class['fields'] = [
                    'id' => [
                        'name' => 'id',
                        'resource' => null,
                        'range' => $range,
                        'cardinality' => CardinalitiesExtractor::CARDINALITY_1_1,
                        'ormColumn' => null,
                        'isArray' => false,
                        'isReadable' => true,
                        'isWritable' => $writable,
                        'isNullable' => $nullable,
                        'isUnique' => false,
                        'isCustom' => true,
                        'isEnum' => false,
                        'isId' => true,
                        'typeHint' => $typeHint,
                    ],
                ] + $class['fields'];
            }
        }

        // Initialize annotation generators
        $annotationGenerators = [];
        foreach ($config['annotationGenerators'] as $annotationGenerator) {
            $generator = new $annotationGenerator($this->logger, $this->graphs, $this->cardinalities, $config, $classes);

            $annotationGenerators[] = $generator;
        }

        $interfaceMappings = [];
        $generatedFiles = [];

        foreach ($classes as $className => &$class) {
            $class['uses'] = $this->generateClassUses($annotationGenerators, $classes, $className);
            $class['annotations'] = $this->generateClassAnnotations($annotationGenerators, $className);
            if (false === isset($typesToGenerate[$className])) {
                $class['interfaceAnnotations'] = $this->generateInterfaceAnnotations($annotationGenerators, $className);
            }

            foreach ($class['constants'] as $constantName => $constant) {
                $class['constants'][$constantName]['annotations'] = $this->generateConstantAnnotations($annotationGenerators, $className, $constantName);
            }

            foreach ($class['fields'] as $fieldName => &$field) {
                $field['annotations'] = $this->generateFieldAnnotations($annotationGenerators, $className, $fieldName);
                $field['getterAnnotations'] = $this->generateGetterAnnotations($annotationGenerators, $className, $fieldName);

                if ($field['isArray']) {
                    $field['adderAnnotations'] = $this->generateAdderAnnotations($annotationGenerators, $className, $fieldName);
                    $field['removerAnnotations'] = $this->generateRemoverAnnotations($annotationGenerators, $className, $fieldName);
                } else {
                    $field['setterAnnotations'] = $this->generateSetterAnnotations($annotationGenerators, $className, $fieldName);
                }
            }

            $classDir = $this->namespaceToDir($config, $class['namespace']);

            if (!file_exists($classDir)) {
                mkdir($classDir, 0777, true);
            }

            $path = sprintf('%s%s.php', $classDir, $className);
            $generatedFiles[] = $path;

            file_put_contents(
                $path,
                $this->twig->render('class.php.twig', [
                    'config' => $config,
                    'class' => $class,
                ])
            );

            if (isset($class['interfaceNamespace'])) {
                $interfaceDir = $this->namespaceToDir($config, $class['interfaceNamespace']);

                if (!file_exists($interfaceDir)) {
                    mkdir($interfaceDir, 0777, true);
                }

                $path = sprintf('%s%s.php', $interfaceDir, $class['interfaceName']);
                $generatedFiles[] = $path;
                file_put_contents(
                    $path,
                    $this->twig->render('interface.php.twig', [
                        'config' => $config,
                        'class' => $class,
                    ])
                );

                if ($config['doctrine']['resolveTargetEntityConfigPath'] && !$class['abstract']) {
                    $interfaceMappings[$class['interfaceNamespace'].'\\'.$class['interfaceName']] = $class['namespace'].'\\'.$className;
                }
            }
        }

        if (!$interfaceMappings && $config['doctrine']['resolveTargetEntityConfigPath']) {
            $file = $config['output'].'/'.$config['doctrine']['resolveTargetEntityConfigPath'];
            $dir = dirname($file);
            if (!file_exists($dir)) {
                mkdir($dir, 0777, true);
            }

            file_put_contents(
                $file,
                $this->twig->render('doctrine.xml.twig', ['mappings' => $interfaceMappings])
            );

            $generatedFiles[] = $file;
        }

        $this->fixCs($generatedFiles);
    }
public function generate(array $config): void
    {
        $baseClass = [
            'constants' => [],
            'fields' => [],
            'uses' => [],
            'hasConstructor' => false,
            'parentHasConstructor' => false,
            'hasChild' => false,
            'abstract' => false,
        ];

        $typesToGenerate = [];

        if (!$config['types']) {
            foreach ($this->graphs as $graph) {
                $typesToGenerate = $graph->allOfType('rdfs:Class');
            }
        } else {
            foreach ($config['types'] as $typeName => $typeConfig) {
                $resource = null;
                foreach ($this->graphs as $graph) {
                    $resources = $graph->resources();

                    if (isset($resources[$typeConfig['vocabularyNamespace'].$typeName])) {
                        $resource = $graph->resource($typeConfig['vocabularyNamespace'].$typeName, 'rdfs:Class');
                        break;
                    }
                }

                if ($resource) {
                    $typesToGenerate[$typeName] = $resource;
                } elseif (empty($graph)) {
                    $this->logger->warning('Type "{typeName}" cannot be found and there is no graph to generate entity.');
                } else {
                    $this->logger->warning('Type "{typeName}" cannot be found. Using "{guessFrom}" type to generate entity.', ['typeName' => $typeName, 'guessFrom' => $typeConfig['guessFrom']]);
                    $type = $graph->resource($typeConfig['vocabularyNamespace'].$typeConfig['guessFrom'], 'rdfs:Class');
                    $typesToGenerate[$typeName] = $type;
                }
            }
        }

        $classes = [];
        $propertiesMap = $this->createPropertiesMap($typesToGenerate);

        foreach ($typesToGenerate as $typeName => $type) {
            $typeName = is_string($typeName) ? $typeName : $type->localName();
            $typeConfig = $config['types'][$typeName] ?? null;
            $class = $baseClass;

            $comment = $type->get('rdfs:comment');

            $class['name'] = $typeName;
            $class['label'] = $comment ? $comment->getValue() : '';
            $class['resource'] = $type;
            $class['config'] = $typeConfig;

            $class['isEnum'] = $this->isEnum($type);
            if ($class['isEnum']) {
                $class['namespace'] = $typeConfig['namespace'] ?? $config['namespaces']['enum'];
                $class['parent'] = 'Enum';
                $class['uses'][] = Enum::class;

                // Constants
                foreach ($this->graphs as $graph) {
                    foreach ($graph->allOfType($type->getUri()) as $instance) {
                        $class['constants'][$instance->localName()] = [
                            'name' => strtoupper(substr(preg_replace('/([A-Z])/', '_$1', $instance->localName()), 1)),
                            'resource' => $instance,
                            'value' => $instance->getUri(),
                        ];
                    }
                }
            } else {
                // Entities
                $class['namespace'] = $typeConfig['namespaces']['class'] ?? $config['namespaces']['entity'];

                // Parent
                $class['parent'] = $typeConfig['parent'] ?? null;
                if (null === $class['parent']) {
                    $numberOfSupertypes = count($type->all('rdfs:subClassOf'));

                    if ($numberOfSupertypes > 1) {
                        $this->logger->warning(sprintf('The type "%s" has several supertypes. Using the first one.', $type->localName()));
                    }

                    $class['parent'] = $numberOfSupertypes ? $type->all('rdfs:subClassOf')[0]->localName() : false;
                }

                if (isset($class['parent']) && isset($config['types'][$class['parent']]['namespaces']['class'])) {
                    $parentNamespace = $config['types'][$class['parent']]['namespaces']['class'];

                    if ($parentNamespace !== $class['namespace']) {
                        $class['uses'][] = $parentNamespace.'\\'.$class['parent'];
                    }
                }

                // Embeddable
                $class['embeddable'] = $typeConfig['embeddable'] ?? false;

                if (!$config['types'] && $class['parent'] && !isset($config['types'][$class['parent']])) {
                    $this->logger->error(sprintf('The type "%s" (parent of "%s") doesn\'t exist', $class['parent'], $type->localName()));
                }

                // Interfaces
                if ($config['useInterface']) {
                    $class['interfaceNamespace'] = isset($typeConfig['namespaces']['interface']) && $typeConfig['namespaces']['interface'] ? $typeConfig['namespaces']['interface'] : $config['namespaces']['interface'];
                    $class['interfaceName'] = sprintf('%sInterface', $typeName);
                }
            }

            // Fields
            if (!$typeConfig['allProperties'] && isset($typeConfig['properties']) && is_array($typeConfig['properties'])) {
                foreach ($typeConfig['properties'] as $key => $value) {
                    foreach ($propertiesMap[$type->getUri()] as $property) {
                        if ($key !== $property->localName()) {
                            continue;
                        }

                        $class = $this->generateField($config, $class, $type, $typeName, $property->localName(), $property);
                        continue 2;
                    }

                    // Add custom fields (non schema.org)
                    $this->logger->info(sprintf('The property "%s" (type "%s") is a custom property.', $key, $type->localName()));
                    $class = $this->generateField($config, $class, $type, $typeName, $key);
                }
            } else {
                // All properties
                foreach ($propertiesMap[$type->getUri()] as $property) {
                    $class = $this->generateField($config, $class, $type, $typeName, $property->localName(), $property);
                }
            }

            $classes[$typeName] = $class;
        }

        // Second pass
        foreach ($classes as &$class) {
            if ($class['parent'] && isset($classes[$class['parent']])) {
                $classes[$class['parent']]['hasChild'] = true;
                $class['parentHasConstructor'] = $classes[$class['parent']]['hasConstructor'];
            }

            foreach ($class['fields'] as &$field) {
                $field['isEnum'] = isset($classes[$field['range']]) && $classes[$field['range']]['isEnum'];
                $field['typeHint'] = $this->fieldToTypeHint($config, $field, $classes) ?? false;

                if ($field['isArray']) {
                    $field['adderRemoverTypeHint'] = $this->fieldToAdderRemoverTypeHint($field, $classes) ?? false;
                }
            }
        }

        // Third pass
        foreach ($classes as &$class) {
            $class['abstract'] = $config['types'][$class['name']]['abstract'] ?? $class['hasChild'];

            // When including all properties, ignore properties already set on parent
            if (isset($config['types'][$class['name']]['allProperties']) && $config['types'][$class['name']]['allProperties'] && isset($classes[$class['parent']])) {
                $type = $class['resource'];

                foreach ($propertiesMap[$type->getUri()] as $property) {
                    if (!isset($class['fields'][$property->localName()])) {
                        continue;
                    }

                    $parentConfig = $config['types'][$class['parent']] ?? null;
                    $parentClass = $classes[$class['parent']];

                    while ($parentClass) {
                        if (!isset($parentConfig['properties']) || !is_array($parentConfig['properties'])) {
                            // Unset implicit property
                            $parentType = $parentClass['resource'];
                            if (in_array($property, $propertiesMap[$parentType->getUri()], true)) {
                                unset($class['fields'][$property->localName()]);
                                continue 2;
                            }
                        } else {
                            // Unset explicit property
                            if (array_key_exists($property->localName(), $parentConfig['properties'])) {
                                unset($class['fields'][$property->localName()]);
                                continue 2;
                            }
                        }

                        $parentConfig = $parentClass['parent'] ? ($config['types'][$parentClass['parent']] ?? null) : null;
                        $parentClass = $parentClass['parent'] ? $classes[$parentClass['parent']] : null;
                    }
                }
            }
        }

        // Generate ID
        if ($config['id']['generate']) {
            foreach ($classes as &$class) {
                if ($class['hasChild'] || $class['isEnum'] || $class['embeddable']) {
                    continue;
                }

                switch ($config['id']['generationStrategy']) {
                    case 'auto':
                        $range = 'Integer';
                        $typeHint = 'int';
                        $writable = false;
                        $nullable = true;
                        break;
                    case 'uuid':
                        $range = 'Text';
                        $typeHint = 'string';
                        $writable = $config['id']['writable'];
                        $nullable = !$writable;
                        break;
                    case 'mongoid':
                        $range = 'Text';
                        $typeHint = 'string';
                        $writable = false;
                        $nullable = true;
                        break;
                    default:
                        $range = 'Text';
                        $typeHint = 'string';
                        $writable = true;
                        $nullable = false;
                        break;
                }

                $class['fields'] = [
                    'id' => [
                        'name' => 'id',
                        'resource' => null,
                        'range' => $range,
                        'cardinality' => CardinalitiesExtractor::CARDINALITY_1_1,
                        'ormColumn' => null,
                        'isArray' => false,
                        'isReadable' => true,
                        'isWritable' => $writable,
                        'isNullable' => $nullable,
                        'isUnique' => false,
                        'isCustom' => true,
                        'isEnum' => false,
                        'isId' => true,
                        'typeHint' => $typeHint,
                    ],
                ] + $class['fields'];
            }
        }

        // Initialize annotation generators
        $annotationGenerators = [];
        foreach ($config['annotationGenerators'] as $annotationGenerator) {
            $generator = new $annotationGenerator($this->logger, $this->graphs, $this->cardinalities, $config, $classes);

            $annotationGenerators[] = $generator;
        }

        $interfaceMappings = [];
        $generatedFiles = [];

        foreach ($classes as $className => &$class) {
            $class['uses'] = $this->generateClassUses($annotationGenerators, $classes, $className);
            $class['annotations'] = $this->generateClassAnnotations($annotationGenerators, $className);
            if (false === isset($typesToGenerate[$className])) {
                $class['interfaceAnnotations'] = $this->generateInterfaceAnnotations($annotationGenerators, $className);
            }

            foreach ($class['constants'] as $constantName => $constant) {
                $class['constants'][$constantName]['annotations'] = $this->generateConstantAnnotations($annotationGenerators, $className, $constantName);
            }

            foreach ($class['fields'] as $fieldName => &$field) {
                $field['annotations'] = $this->generateFieldAnnotations($annotationGenerators, $className, $fieldName);
                $field['getterAnnotations'] = $this->generateGetterAnnotations($annotationGenerators, $className, $fieldName);

                if ($field['isArray']) {
                    $field['adderAnnotations'] = $this->generateAdderAnnotations($annotationGenerators, $className, $fieldName);
                    $field['removerAnnotations'] = $this->generateRemoverAnnotations($annotationGenerators, $className, $fieldName);
                } else {
                    $field['setterAnnotations'] = $this->generateSetterAnnotations($annotationGenerators, $className, $fieldName);
                }
            }

            $classDir = $this->namespaceToDir($config, $class['namespace']);

            if (!file_exists($classDir)) {
                mkdir($classDir, 0777, true);
            }

            $path = sprintf('%s%s.php', $classDir, $className);
            $generatedFiles[] = $path;

            file_put_contents(
                $path,
                $this->twig->render('class.php.twig', [
                    'config' => $config,
                    'class' => $class,
                ])
            );

            if (isset($class['interfaceNamespace'])) {
                $interfaceDir = $this->namespaceToDir($config, $class['interfaceNamespace']);

                if (!file_exists($interfaceDir)) {
                    mkdir($interfaceDir, 0777, true);
                }

                $path = sprintf('%s%s.php', $interfaceDir, $class['interfaceName']);
                $generatedFiles[] = $path;
                file_put_contents(
                    $path,
                    $this->twig->render('interface.php.twig', [
                        'config' => $config,
                        'class' => $class,
                    ])
                );

                if ($config['doctrine']['resolveTargetEntityConfigPath'] && !$class['abstract']) {
                    $interfaceMappings[$class['interfaceNamespace'].'\\'.$class['interfaceName']] = $class['namespace'].'\\'.$className;
                }
            }
        }

        if (!$interfaceMappings && $config['doctrine']['resolveTargetEntityConfigPath']) {
            $file = $config['output'].'/'.$config['doctrine']['resolveTargetEntityConfigPath'];
            $dir = dirname($file);
            if (!file_exists($dir)) {
                mkdir($dir, 0777, true);
            }

            file_put_contents(
                $file,
                $this->twig->render('doctrine.xml.twig', ['mappings' => $interfaceMappings])
            );

            $generatedFiles[] = $file;
        }

        $this->fixCs($generatedFiles);
    }
Generates files.