An error occurred while loading the file. Please try again.
-
Le Roux Erwan authoredd862d32f
<?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-2020 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 Assert\Assertion;
use Irstea\NgModelGeneratorBundle\Exceptions\DomainException;
use Irstea\NgModelGeneratorBundle\Metadata\PropertyMetadata;
use Irstea\NgModelGeneratorBundle\Metadata\RepresentationMetadata;
use Irstea\NgModelGeneratorBundle\Metadata\SerializationMetadata;
use Irstea\NgModelGeneratorBundle\Models\ClassInfo;
use Irstea\NgModelGeneratorBundle\Models\ClassName;
use Irstea\NgModelGeneratorBundle\Models\PHPClass;
use Irstea\NgModelGeneratorBundle\Models\Types\Alias;
use Irstea\NgModelGeneratorBundle\Models\Types\ArrayType;
use Irstea\NgModelGeneratorBundle\Models\Types\BuiltinType;
use Irstea\NgModelGeneratorBundle\Models\Types\Deferred;
use Irstea\NgModelGeneratorBundle\Models\Types\Objects\Property;
use Irstea\NgModelGeneratorBundle\Models\Types\Reference;
use Irstea\NgModelGeneratorBundle\Models\Types\Resources\IRI;
use Irstea\NgModelGeneratorBundle\Models\Types\Resources\Representation;
use Irstea\NgModelGeneratorBundle\Models\Types\StringConst;
use Irstea\NgModelGeneratorBundle\Models\Types\Type;
use Irstea\NgModelGeneratorBundle\Models\Types\Union;
use Symfony\Component\PropertyInfo\Type as APIType;
/**
* Class SerializationMapper.
*/
final class SerializationMapper implements TypeFactoryInterface
{
/** @var TypeFactoryInterface */
private $typeFactory;
/** @var SerializationMetadata */
private $serialization;
/** @var ClassInfo[]|null */
private $classInfo;
/** @var bool */
private $withAtFields;
/**
* SerializationMapper constructor.
*/
public function __construct(
TypeFactoryInterface $typeFactory,
SerializationMetadata $serialization,
bool $withAtFields
) {
$this->typeFactory = $typeFactory;
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
$this->serialization = $serialization;
$this->withAtFields = $withAtFields;
}
/**
* {@inheritdoc}
*/
public function has(string $name): bool
{
return $this->typeFactory->has($name);
}
/**
* {@inheritdoc}
*/
public function add(string $name, Type $type): void
{
$this->typeFactory->add($name, $type);
}
/**
* {@inheritdoc}
*/
public function defer(string $name): Deferred
{
return $this->typeFactory->defer($name);
}
/**
* {@inheritdoc}
*/
public function get(string $name): Type
{
if (class_exists($name)) {
$class = PHPClass::get($name);
if ($this->serialization->hasRepresentationOf($class)) {
$repr = $this->serialization->getRepresentationOf($class);
return $this->deferredMapping($name, $repr->getName(), function () use ($repr) {
return $this->mapRepresentation($repr);
});
}
}
return $this->typeFactory->get($name);
}
private function deferredMapping(string $name, string $actualName, callable $mapper): Type
{
return $this->defer($actualName)
->resolveWith(
function () use ($name, $mapper) {
try {
return $mapper();
} catch (\Throwable $ex) {
throw new DomainException(sprintf('error with %s: %s', $name, $ex->getMessage()), 0, $ex);
}
}
);
}
public function getResourceData(): array
{
$resource = $this->serialization->getRoot();
$resourceName = $resource->getFullName();
$classInfo = $this->getClassInfo($resource);
$properties = array_map([$this, 'mapProperty'], $classInfo->getVirtualProperties());
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
$identifiers = array_filter(
$properties,
function (Property $property) {
return $property->isIdentifier();
}
);
$identifier = $identifiers ? array_shift($identifiers) : null;
return [$this->get($resourceName), $identifier, $properties];
}
private function mapRepresentation(RepresentationMetadata $repr): Type
{
$classInfo = $this->getClassInfo($repr);
if ($classInfo->isUndefined()) {
throw new DomainException(sprintf('%s has not been rearranged', $repr));
}
$desc = [];
$desc[] = 'Resource: ' . $repr;
$desc[] = 'Direction: ' . ($this->serialization->isNormalization() ? 'response' : 'request');
$desc[] = sprintf('Serialization groups: %s', implode(', ', $this->serialization->getGroups()) ?: '-');
$desc = trim(implode("\n", $desc));
if ($classInfo->isUnion()) {
/** @var Type[] $types */
$types = [];
foreach ($classInfo->iterateInterfaceDescendants() as $child) {
$types[] = $this->get($child->getFullName());
}
switch (\count($types)) {
case 0:
throw new DomainException(sprintf('Union with no children: %s', $repr));
case 1:
$ref = new Reference($types[0]->getUsage());
$ref->resolve($types[0]);
return $ref;
default:
return new Alias($repr->getName(), Union::create($types), $desc);
}
}
if ($classInfo->isIRI()) {
return $this->createIRI([$classInfo]);
}
$parent = null;
$parentInfo = $classInfo->getParent();
if ($parentInfo !== null && $parentInfo->isInterface()) {
$parent = $this->get($parentInfo->getFullName());
}
$properties = $this->mapProperties($classInfo);
$children = [];
/* @var ClassName $class */
foreach ($classInfo->getChildren() as $child) {
if ($child->isInterface()) {
$children[] = $this->get($child->getFullName());
}
}
return new Representation($repr, $repr->getName(), $parent, $properties, $desc, $children);
}
private function createIRI(array $resources): IRI
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
{
return new IRI(
array_map(
function (ClassName $class) {
return $this->typeFactory->get($class->getFullName());
},
$resources
)
);
}
private function mapProperties(ClassInfo $classInfo): array
{
$properties = [];
$identifierCount = 0;
foreach ($classInfo->getConcreteProperties() as $propertyMeta) {
/** @var PropertyMetadata $propertyMeta */
if ($propertyMeta->isIdentifier()) {
++$identifierCount;
}
$property = $this->mapProperty($propertyMeta);
$properties[$property->getName()] = $property;
}
if ($identifierCount > 1) {
throw new DomainException(sprintf('Resource %s must have at most one identifier, found %d', $classInfo->getBaseName(), $identifierCount));
}
if ($this->withAtFields && $classInfo->isResource()) {
$properties['@id'] = new Property(
'@id',
'',
$this->createIRI(\iterator_to_array($classInfo->iterateConcreteDescendants())),
true,
!$this->serialization->isNormalization(),
true
);
$properties['@type'] = new Property(
'@type',
'',
$this->buildTypeEnumFor($classInfo),
false,
!$this->serialization->isNormalization(),
true
);
}
return $properties;
}
private function buildTypeEnumFor(ClassInfo $classInfo): Type
{
$types = [];
foreach ($classInfo->iterateConcreteDescendants() as $child) {
$types[] = StringConst::get($child->getBaseName());
}
return Union::create($types);
}
private function mapProperty(PropertyMetadata $propertyMeta): Property
{
return new Property(
$propertyMeta->getName(),
$propertyMeta->getDescription() ?: '',
$this->mapType($propertyMeta->getType(), $propertyMeta->isLink() && !$propertyMeta->isEmbedded()),
$propertyMeta->isIdentifier(),
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
$propertyMeta->isNullable(),
!$propertyMeta->isWritable()
);
}
/**
* @param APIType $type
*/
private function mapType(?APIType $type, bool $isLink = false): Type
{
if ($type === null) {
return BuiltinType::get('unknown');
}
if ($type->isCollection() || $type->getBuiltinType() === 'array') {
return $this->mapCollection($type, $isLink);
}
$className = $type->getClassName();
if ($className === null) {
return $this->get($type->getBuiltinType());
}
if ($isLink) {
return $this->createIRI([PHPClass::get($className)]);
}
return $this->get($className);
}
/**
* @param APIType $type
*/
private function mapCollection(?APIType $type, bool $isLink = false): Type
{
$indexType = $type->getCollectionKeyType();
if (!$indexType || $indexType->getBuiltinType() === 'int') {
return new ArrayType($this->mapType($type->getCollectionValueType(), $isLink));
}
throw new DomainException('Cannot handle collection with non-integer index');
}
private function getClassInfo(ClassName $class): ClassInfo
{
if ($this->classInfo === null) {
$this->init();
}
$name = $class->getFullName();
$info = $this->classInfo[$name] ?? null;
Assertion::notNull($info, 'Unknown class $name');
return $info;
}
private function init(): void
{
$reprs = $this->serialization->getRepresentations();
$this->classInfo = [];
// Crée les instances de métadonnées sur les ressources
foreach ($reprs as $className => $repr) {
$this->classInfo[$className] = new ClassInfo(
$repr,
$repr->getProperties(),
$repr->isAbstract(),
$repr->isResource()
);
}
351352353354355356357358359360361362363364365366367
// Crée les liens de parenté
foreach ($reprs as $className => $repr) {
$classInfo = $this->classInfo[$className];
$parent = $repr->getParent();
if ($parent) {
$classInfo->setParent($this->classInfo[$parent->getFullName()]);
}
}
// Optimise les hierarchies
foreach ($this->classInfo as $className => $classInfo) {
$classInfo->rearrangeHiearchy();
}
}
}