soliantconsulting/SimpleFM

View on GitHub
src/Repository/Builder/Metadata/MetadataBuilder.php

Summary

Maintainability
C
1 day
Test Coverage
A
100%
<?php
declare(strict_types = 1);

namespace Soliant\SimpleFM\Repository\Builder\Metadata;

use Assert\Assertion;
use Cache\Adapter\Common\CacheItem;
use Cache\Adapter\Void\VoidCachePool;
use DOMDocument;
use Psr\Cache\CacheItemPoolInterface;
use SimpleXMLElement;
use Soliant\SimpleFM\Repository\Builder\Metadata\Exception\InvalidFileException;
use Soliant\SimpleFM\Repository\Builder\Metadata\Exception\InvalidTypeException;
use Soliant\SimpleFM\Repository\Builder\Metadata\Exception\MissingInterfaceException;
use Soliant\SimpleFM\Repository\Builder\Type;
use Soliant\SimpleFM\Repository\Builder\Type\TypeInterface;

final class MetadataBuilder implements MetadataBuilderInterface
{
    const SCHEMA_PATH = __DIR__ . '/../../../../xsd/entity-metadata-5-1.xsd';

    /**
     * @var string
     */
    private $xmlFolder;

    /**
     * @var TypeInterface[]
     */
    private $types;

    /**
     * @var Entity[]
     */
    private $metadata = [];

    /**
     * @var CacheItemPoolInterface
     */
    private $cache;

    public function __construct(string $xmlFolder, array $additionalTypes = [], CacheItemPoolInterface $cache = null)
    {
        if (!empty($additionalTypes)) {
            Assertion::count(array_filter($additionalTypes, function ($type) : bool {
                return !$type instanceof TypeInterface;
            }), 0, sprintf('At least one element in array is not an instance of %s', TypeInterface::class));
        }

        $this->xmlFolder = $xmlFolder;
        $this->types = $additionalTypes + $this->createBuiltInTypes();
        $this->cache = $cache ?: new VoidCachePool();
    }

    public function getMetadata(string $entityClassName) : Entity
    {
        if (array_key_exists($entityClassName, $this->metadata)) {
            return $this->metadata[$entityClassName];
        }

        $cacheKey = sprintf('simplefm.metadata.%s', md5($entityClassName));

        if ($this->cache->hasItem($cacheKey)) {
            return ($this->metadata[$entityClassName] = $this->cache->getItem($cacheKey)->get());
        }

        $xmlPath = sprintf('%s/%s', $this->xmlFolder, $this->buildFilename($entityClassName));

        if (!file_exists($xmlPath)) {
            throw InvalidFileException::fromNonExistentFile($xmlPath, $entityClassName);
        }

        $entityMetadata = $this->buildMetadata($xmlPath);

        $this->cache->save(new CacheItem($cacheKey, true, $entityMetadata));

        return ($this->metadata[$entityClassName] = $entityMetadata);
    }

    private function createBuiltInTypes() : array
    {
        return [
            'boolean' => new Type\BooleanType(),
            'date-time' => new Type\DateTimeType(),
            'date' => new Type\DateType(),
            'decimal' => new Type\DecimalType(),
            'float' => new Type\FloatType(),
            'integer' => new Type\IntegerType(),
            'nullable-string' => new Type\NullableStringType(),
            'stream' => new Type\StreamType(),
            'string' => new Type\StringType(),
            'time' => new Type\TimeType(),
        ];
    }

    private function buildMetadata(string $xmlPath) : Entity
    {
        $xml = $this->loadValidatedXml($xmlPath);
        $fields = [];
        $embeddables = [];
        $oneToMany = [];
        $manyToOne = [];
        $oneToOne = [];
        $recordId = null;

        if (isset($xml->field)) {
            foreach ($xml->field as $field) {
                $type = (string) $field['type'];

                if (!array_key_exists($type, $this->types)) {
                    throw InvalidTypeException::fromNonExistentType($type);
                }

                $fields[] = new Field(
                    (string) $field['name'],
                    (string) $field['property'],
                    $this->types[$type],
                    (isset($field['repeatable']) && (string) $field['repeatable'] === 'true'),
                    (isset($field['read-only']) && (string) $field['read-only'] === 'true')
                );
            }
        }

        if (isset($xml->embeddable)) {
            foreach ($xml->embeddable as $embeddable) {
                $embeddables[] = new Embeddable(
                    (string) $embeddable['property'],
                    (string) $embeddable['field-name-prefix'],
                    $this->getMetadata((string) $embeddable['class-name'])
                );
            }
        }

        if (isset($xml->{'one-to-many'})) {
            foreach ($xml->{'one-to-many'} as $relation) {
                $oneToMany[] = new OneToMany(
                    (string) $relation['property'],
                    (string) $relation['target-table'],
                    (string) $relation['target-entity'],
                    (string) $relation['target-field-name'],
                    (isset($relation['eager-hydration']) && (string) $relation['eager-hydration'] === 'true')
                );
            }
        }

        if (isset($xml->{'many-to-one'})) {
            foreach ($xml->{'many-to-one'} as $relation) {
                $manyToOne[] = new ManyToOne(
                    (string) $relation['name'],
                    (string) $relation['property'],
                    (string) $relation['target-table'],
                    (string) $relation['target-entity'],
                    (string) $relation['target-property-name'],
                    (string) $relation['target-field-name'],
                    $this->getInterfaceNameForRelation(
                        (string) $xml['class-name'],
                        (string) $relation['target-entity'],
                        (isset($relation['eager-hydration']) && (string) $relation['eager-hydration'] === 'true')
                    ),
                    (isset($relation['read-only']) && (string) $relation['read-only'] === 'true'),
                    (isset($relation['eager-hydration']) && (string) $relation['eager-hydration'] === 'true')
                );
            }
        }

        if (isset($xml->{'one-to-one-owning'})) {
            foreach ($xml->{'one-to-one-owning'} as $relation) {
                $oneToOne[] = new OneToOne(
                    (string) $relation['property'],
                    (string) $relation['target-table'],
                    (string) $relation['target-entity'],
                    (string) $relation['target-field-name'],
                    $this->getInterfaceNameForRelation(
                        (string) $xml['class-name'],
                        (string) $relation['target-entity'],
                        (isset($relation['eager-hydration']) && (string) $relation['eager-hydration'] === 'true')
                    ),
                    true,
                    (isset($relation['read-only']) && (string) $relation['read-only'] === 'true'),
                    (string) $relation['name'],
                    (string) $relation['target-property-name'],
                    (isset($relation['eager-hydration']) && (string) $relation['eager-hydration'] === 'true')
                );
            }
        }

        if (isset($xml->{'one-to-one-inverse'})) {
            foreach ($xml->{'one-to-one-inverse'} as $relation) {
                $oneToOne[] = new OneToOne(
                    (string) $relation['property'],
                    (string) $relation['target-table'],
                    (string) $relation['target-entity'],
                    (string) $relation['target-field-name'],
                    $this->getInterfaceNameForRelation(
                        (string) $xml['class-name'],
                        (string) $relation['target-entity'],
                        (isset($relation['eager-hydration']) && (string) $relation['eager-hydration'] === 'true')
                    ),
                    false,
                    false,
                    null,
                    null,
                    (isset($relation['eager-hydration']) && (string) $relation['eager-hydration'] === 'true')
                );
            }
        }

        if (isset($xml->{'record-id'})) {
            $recordId = new RecordId((string) $xml->{'record-id'}['property']);
        }

        return new Entity(
            (string) $xml['layout'],
            (string) $xml['class-name'],
            $fields,
            $embeddables,
            $oneToMany,
            $manyToOne,
            $oneToOne,
            $recordId,
            isset($xml['interface-name']) ? (string) $xml['interface-name'] : null
        );
    }

    private function loadValidatedXml(string $xmlPath) : SimpleXMLElement
    {
        $previousUseInternalErrors = libxml_use_internal_errors(true);
        $xml = new DOMDocument();
        $loadResult = $xml->load($xmlPath);

        if (!$loadResult || !$xml->schemaValidate(self::SCHEMA_PATH)) {
            throw InvalidFileException::fromInvalidFile($xmlPath);
        }

        libxml_use_internal_errors($previousUseInternalErrors);
        return simplexml_import_dom($xml);
    }

    private function buildFilename(string $className) : string
    {
        return str_replace('\\', '.', $className) . '.xml';
    }

    /**
     * @return string|null
     */
    private function getInterfaceNameForRelation(
        string $mainEntityClassName,
        string $relationEntityClassName,
        bool $eagerHydration
    ) {
        $entityMetadata = $this->getMetadata($relationEntityClassName);

        if ($entityMetadata->hasInterfaceName()) {
            return $entityMetadata->getInterfaceName();
        }

        if ($eagerHydration) {
            return null;
        }

        throw MissingInterfaceException::fromMissingInterface($mainEntityClassName, $relationEntityClassName);
    }
}