SerializationMapper.php 11.49 KiB
<?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(); } } }