An error occurred while loading the file. Please try again.
-
Guillaume Perréal authored0ad6f764
<?php declare(strict_types=1);
/*
* This file is part of "irstea/ng-model-generator-bundle".
*
* "irstea/ng-model-generator-bundle" generates Typescript interfaces for Angular using api-platform metadata.
* Copyright (C) 2018-2019 IRSTEA
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License and the GNU
* Lesser General Public License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
namespace Irstea\NgModelGeneratorBundle;
use ApiPlatform\Core\Documentation\Documentation;
use Irstea\NgModelGeneratorBundle\Exceptions\DomainException;
use Irstea\NgModelGeneratorBundle\Exceptions\Exception;
use Irstea\NgModelGeneratorBundle\Iterators\IteratorBuilder;
use Irstea\NgModelGeneratorBundle\Metadata\MetadataFactoryInterface;
use Irstea\NgModelGeneratorBundle\Metadata\ResourceMetadata;
use Irstea\NgModelGeneratorBundle\Models\ClassName;
use Irstea\NgModelGeneratorBundle\Models\Declaration;
use Irstea\NgModelGeneratorBundle\Models\PHPClass;
use Irstea\NgModelGeneratorBundle\Models\Types\Alias;
use Irstea\NgModelGeneratorBundle\Models\Types\BuiltinType;
use Irstea\NgModelGeneratorBundle\Models\Types\Objects\InterfaceType;
use Irstea\NgModelGeneratorBundle\Models\Types\Objects\Property;
use Irstea\NgModelGeneratorBundle\Models\Types\Objects\Repository;
use Irstea\NgModelGeneratorBundle\Models\Types\Resources\UUID;
use Irstea\NgModelGeneratorBundle\Models\Types\StringConst;
use Irstea\NgModelGeneratorBundle\Models\Types\Type;
use Irstea\NgModelGeneratorBundle\Models\Types\Union;
use Irstea\NgModelGeneratorBundle\Writers\MultiFileWriter;
use Irstea\NgModelGeneratorBundle\Writers\Writer;
use Symfony\Component\PropertyInfo\Type as PHPType;
use Twig\Environment;
/**
* Class Serializer.
*/
final class ModelGenerator
{
/** @var MetadataFactoryInterface */
private $metadataFactory;
/** @var Environment */
private $twigEnv;
/** @var Documentation */
private $documentation;
/** @var TypeFactory */
private $typeFactory;
/** @var SerializationMapperFactoryInterface */
private $serializationMapperFactory;
/**
* Serializer constructor.
*
* @param MetadataFactoryInterface $metadataFactory
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
* @param Environment $twigEnv
*/
public function __construct(
MetadataFactoryInterface $metadataFactory,
Environment $twigEnv
) {
$this->metadataFactory = $metadataFactory;
$this->twigEnv = $twigEnv;
}
/**
* Génère les modèles Typescript.
* Cette méthode n'est pas réentrante.
*
* @param Documentation $doc
* @param Writer $writer
*/
public function generate(Documentation $doc, MultiFileWriter $writer): void
{
if ($this->documentation !== null) {
throw new DomainException(__METHOD__ . ' is not reentrant');
}
try {
$this->documentation = $doc;
$this->doGenerate($writer);
} finally {
unset(
$this->documentation,
$this->typeFactory
);
}
}
/**
* @param MultiFileWriter $writer
*
* @throws \Twig_Error_Loader
* @throws \Twig_Error_Runtime
* @throws \Twig_Error_Syntax
*
* @return string
*/
private function doGenerate(MultiFileWriter $writer): void
{
$this->typeFactory = $this->createTypeFactory();
$this->serializationMapperFactory = new SerializationMapperFactory($this->typeFactory);
$context = [
'title' => $this->documentation->getTitle(),
'version' => $this->documentation->getVersion(),
'description' => $this->documentation->getDescription() ?: '',
'repositories' => $this->extractRepositories(),
'repoImports' => [],
'declarations' => [],
];
$isResourceDeclaration = function ($item) {
return $item instanceof Declaration
&& !($item instanceof Repository)
&& !\in_array($item->getName(), ['UUID', 'IRI', 'Collection', 'DateTime'], true);
};
foreach (
IteratorBuilder::from($context['repositories'])
->unique()
->recurse(\RecursiveIteratorIterator::CHILD_FIRST)
->where($isResourceDeclaration)
as $type) {
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
$context['declarations'][$type->getName()] = $type;
}
foreach (
IteratorBuilder::from($context['repositories'])
->unique()
->recurseWhere(
\RecursiveIteratorIterator::SELF_FIRST,
function ($item) {
return $item instanceof Repository
|| !($item instanceof Declaration);
}
)
->where($isResourceDeclaration)
as $type) {
$context['repoImports'][] = $type->getUsage();
}
sort($context['repoImports']);
$this->generateFile($writer, 'common.ts', $context);
$this->generateFile($writer, 'resources.ts', $context);
$this->generateFile($writer, 'repositories.ts', $context);
$this->generateFile($writer, 'metadata.ts', $context);
$this->generateFile($writer, 'index.ts', $context);
}
/**
* @param MultiFileWriter $writer
*
* @throws \Twig_Error_Loader
* @throws \Twig_Error_Runtime
* @throws \Twig_Error_Syntax
*
* @return string
*/
private function generateFile(MultiFileWriter $writer, string $path, array $context): void
{
$fileWriter = $writer->newFile($path);
try {
$template = sprintf('@NgModelGenerator/%s.twig', basename($path));
$context['filename'] = $path;
$fileWriter->write($this->twigEnv->render($template, $context));
} finally {
$fileWriter->close();
}
}
/**
* Crée une usine à types contenant un certain nombre de types par défaut.
*
* @return TypeFactoryInterface
*/
private function createTypeFactory(): TypeFactoryInterface
{
$factory = new TypeFactory();
foreach (['Array', 'Date', 'boolean', 'number', 'null', 'string', 'any', 'never'] as $builtin) {
$factory->add($builtin, BuiltinType::get($builtin));
}
$factory->add('UUID', UUID::get());
$factory->add('CommonFilters', $this->createCommonFilters('CommonFilters'));
$factory->add('Ordering', $this->createOrdering());
foreach ([
PHPType::BUILTIN_TYPE_ARRAY => 'Array',
PHPType::BUILTIN_TYPE_BOOL => 'boolean',
PHPType::BUILTIN_TYPE_FLOAT => 'number',
PHPType::BUILTIN_TYPE_INT => 'number',
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
PHPType::BUILTIN_TYPE_NULL => 'null',
PHPType::BUILTIN_TYPE_STRING => 'string',
'Ramsey\Uuid\UuidInterface' => 'UUID',
'DateTime' => 'string',
'DateTimeInterface' => 'DateTime',
'DateTimeImmutable' => 'DateTime',
] as $alias => $target) {
if ($target === $alias || $factory->has($alias)) {
continue;
}
$factory->add($alias, $factory->get($target));
}
return $factory;
}
/**
* @param string $name
*
* @return Type
*/
private function createCommonFilters(string $name): Type
{
$properties = [];
$meta = $this->metadataFactory->getPaginationMetadata();
if ($meta->isEnabled()) {
$properties[] = new Property($meta->getPageParameterName(), '', BuiltinType::get('number'), false, true);
if ($meta->isClientItemsPerPage()) {
$properties[] = new Property($meta->getItemsPerPageParameterName(), '', BuiltinType::get('number'), false, true);
}
}
return new InterfaceType($name, null, $properties);
}
/**
* @return Type
*/
private function createOrdering(): Type
{
return new Alias('Ordering', Union::create([StringConst::get('asc'), StringConst::get('desc')]), 'Allowed values for ordering parameters');
}
/**
* Retourne un iterateur sur les métadonnées des ressources.
*
* @return \Iterator
*/
private function getResourceMetadata(): \Iterator
{
$classNames = iterator_to_array($this->documentation->getResourceNameCollection()->getIterator());
$classes = array_map([PHPClass::class, 'get'], $classNames);
usort($classes, [PHPClass::class, 'baseNameOrdering']);
foreach ($classes as $class) {
yield $class => $this->metadataFactory->getResourceMetadata($class);
}
}
/**
* @return Repository[]
*/
private function extractRepositories(): array
{
$repositories = [];
// Premier passage pour prégénérer des référénces aux ressources
foreach ($this->getResourceMetadata() as $class => $resourceMeta) {
/* @var ClassName $class */
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
/* @var ResourceMetadata $resourceMeta */
$ref = $this->typeFactory->defer($class->getFullName());
$this->typeFactory->add($resourceMeta->getBaseName(), $ref);
}
// Maintenant on génére les repositories
foreach ($this->getResourceMetadata() as $class => $resourceMeta) {
/* @var ClassName $class */
/* @var ResourceMetadata $resourceMeta */
try {
$repo = $this->buildRepositories($resourceMeta);
if ($repo) {
$repositories[$repo->getName()] = $repo;
}
} catch (Exception $ex) {
printf("Error while mapping %s: %s\n%s\n", $class, $ex->getMessage(), $ex->getTraceAsString());
}
}
ksort($repositories);
return $repositories;
}
/**
* @param ResourceMetadata $resourceMeta
*
* @return Repository|null
*/
private function buildRepositories(ResourceMetadata $resourceMeta): ?Repository
{
$defaultNormalization = $resourceMeta->getDefaultNormalization();
$defaultNormalizationMapper = $this->serializationMapperFactory->create($defaultNormalization, true);
/**
* @var Type
* @var Property $identifier
* @var Property[] $properties
*/
[$defaultRepr, $identifier, $properties] = $defaultNormalizationMapper->getResourceData();
if (!$properties) {
printf(
"No property found for %s (groups: [%s])\n",
$resourceMeta->getBaseName(),
implode(', ', $defaultNormalization->getGroups())
);
return null;
}
if (!$identifier) {
printf(
"No identifier found for %s (groups: [%s])\n",
$resourceMeta->getBaseName(),
implode(', ', $defaultNormalization->getGroups())
);
return null;
}
$operations = [];
$pathParser = new PathParser($properties);
$opsMeta = $resourceMeta->getOperations();
if (!$opsMeta) {
return null;
}
351352353354355356357358359360361362363364365366367368369370
if (isset($opsMeta['getitem'])) {
$get = $opsMeta['getitem'];
$iri = $pathParser->parse($get->getPath(), $get->getRequirements());
} else {
$iri = null;
}
foreach ($opsMeta as $operation) {
$mapper = new OperationMapper($this->typeFactory, $this->serializationMapperFactory, $pathParser, $operation, $iri);
$operations[] = $mapper();
}
if (!$operations) {
return null;
}
return new Repository($resourceMeta, $defaultRepr, $identifier, $iri, $operations);
}
}