edmondscommerce/doctrine-static-meta

View on GitHub
src/CodeGeneration/NamespaceHelper.php

Summary

Maintainability
B
5 hrs
Test Coverage
<?php

declare(strict_types=1);

namespace EdmondsCommerce\DoctrineStaticMeta\CodeGeneration;

use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Command\AbstractCommand;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\AbstractGenerator;
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator;
use EdmondsCommerce\DoctrineStaticMeta\Config;
use EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException;
use EdmondsCommerce\DoctrineStaticMeta\MappingHelper;
use Exception;
use RuntimeException;
use SplFileInfo;
use Symfony\Component\Finder\Finder;

use function array_merge;
use function array_slice;
use function get_class;
use function implode;
use function in_array;
use function str_replace;
use function strlen;
use function strrpos;
use function substr;
use function ucfirst;

/**
 * Class NamespaceHelper
 *
 * Pure functions for working with namespaces and to calculate namespaces
 *
 * @package EdmondsCommerce\DoctrineStaticMeta\CodeGeneration
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
 * @SuppressWarnings(PHPMD.ExcessivePublicCount)
 */
class NamespaceHelper
{
    public function getAllArchetypeFieldFqns(): array
    {
        $archetypeFqns = [];
        $namespaceBase = 'EdmondsCommerce\\DoctrineStaticMeta\\Entity\\Fields\\Traits';
        $finder        = (new Finder())->files()
                                       ->name('*.php')
                                       ->in(__DIR__ . '/../Entity/Fields/Traits/');
        foreach ($finder as $file) {
            /** @var SplFileInfo $file */
            $realpath = $file->getRealPath();
            if (\ts\stringContains($realpath, '/PrimaryKey/')) {
                continue;
            }
            $subPath         = substr(
                $realpath,
                strpos($realpath, 'Entity/Fields/Traits/') + strlen('Entity/Fields/Traits/')
            );
            $subPath         = substr($subPath, 0, -4);
            $subFqn          = str_replace('/', '\\', $subPath);
            $archetypeFqns[] = $namespaceBase . '\\' . $subFqn;
        }

        return $archetypeFqns;
    }

    public function swapSuffix(string $fqn, string $currentSuffix, string $newSuffix): string
    {
        return $this->cropSuffix($fqn, $currentSuffix) . $newSuffix;
    }

    /**
     * Crop a suffix from an FQN if it is there.
     *
     * If it is not there, do nothing and return the FQN as is
     *
     * @param string $fqn
     * @param string $suffix
     *
     * @return string
     */
    public function cropSuffix(string $fqn, string $suffix): string
    {
        if ($suffix === substr($fqn, -strlen($suffix))) {
            return substr($fqn, 0, -strlen($suffix));
        }

        return $fqn;
    }

    public function getEmbeddableObjectFqnFromEmbeddableObjectInterfaceFqn(string $interfaceFqn): string
    {
        return str_replace(
            ['\\Interfaces\\', 'Interface'],
            ['\\', ''],
            $interfaceFqn
        );
    }

    /**
     * @param mixed|object $object
     *
     * @return string
     */
    public function getObjectShortName($object): string
    {
        return $this->getClassShortName($this->getObjectFqn($object));
    }

    /**
     * @param string $className
     *
     * @return string
     */
    public function getClassShortName(string $className): string
    {
        $exp = explode('\\', $className);

        return end($exp);
    }

    /**
     * @param mixed|object $object
     *
     * @return string
     * @see https://gist.github.com/ludofleury/1708784
     */
    public function getObjectFqn($object): string
    {
        return get_class($object);
    }

    /**
     * Get the basename of a namespace
     *
     * @param string $namespace
     *
     * @return string
     */
    public function basename(string $namespace): string
    {
        $strrpos = strrpos($namespace, '\\');
        if (false === $strrpos) {
            return $namespace;
        }

        return $this->tidy(substr($namespace, $strrpos + 1));
    }

    /**
     * Checks and tidies up a given namespace
     *
     * @param string $namespace
     *
     * @return string
     * @throws RuntimeException
     */
    public function tidy(string $namespace): string
    {
        if (\ts\stringContains($namespace, '/')) {
            throw new RuntimeException('Invalid namespace ' . $namespace);
        }
        #remove repeated separators
        $namespace = preg_replace(
            '#' . '\\\\' . '+#',
            '\\',
            $namespace
        );

        return $namespace;
    }

    /**
     * Get the fully qualified name of the Fixture class for a specified Entity fully qualified name
     *
     * @param string $entityFqn
     *
     * @return string
     */
    public function getFixtureFqnFromEntityFqn(string $entityFqn): string
    {
        return str_replace(
            '\\Entities',
            '\\Assets\\Entity\\Fixtures',
            $entityFqn
        ) . 'Fixture';
    }

    /**
     * Get the fully qualified name of the Entity for a specified Entity fully qualified name
     *
     * @param string $fixtureFqn
     *
     * @return string
     */
    public function getEntityFqnFromFixtureFqn(string $fixtureFqn): string
    {
        return substr(
            str_replace(
                '\\Assets\\Entity\\Fixtures',
                '\\Entities',
                $fixtureFqn
            ),
            0,
            -strlen('Fixture')
        );
    }

    /**
     * Get the namespace root up to and including a specified directory
     *
     * @param string $fqn
     * @param string $directory
     *
     * @return null|string
     */
    public function getNamespaceRootToDirectoryFromFqn(string $fqn, string $directory): ?string
    {
        $strPos = strrpos(
            $fqn,
            $directory
        );
        if (false !== $strPos) {
            return $this->tidy(substr($fqn, 0, $strPos + strlen($directory)));
        }

        return null;
    }

    /**
     * Get the sub path for an Entity file, start from the Entities path - normally `/path/to/project/src/Entities`
     *
     * @param string $entityFqn
     *
     * @return string
     */
    public function getEntityFileSubPath(
        string $entityFqn
    ): string {
        return $this->getEntitySubPath($entityFqn) . '.php';
    }

    /**
     * Get the folder structure for an Entity, start from the Entities path - normally `/path/to/project/src/Entities`
     *
     * This is not the path to the file, but the sub path of directories for storing entity related items.
     *
     * @param string $entityFqn
     *
     * @return string
     */
    public function getEntitySubPath(
        string $entityFqn
    ): string {
        $entityPath = str_replace(
            '\\',
            '/',
            $this->getEntitySubNamespace($entityFqn)
        );

        return '/' . $entityPath;
    }

    /**
     * Get the Namespace for an Entity, start from the Entities Fully Qualified Name base - normally
     * `\My\Project\Entities\`
     *
     * @param string $entityFqn
     *
     * @return string
     */
    public function getEntitySubNamespace(
        string $entityFqn
    ): string {
        return $this->tidy(
            substr(
                $entityFqn,
                strrpos(
                    $entityFqn,
                    '\\' . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\'
                )
                + strlen('\\' . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\')
            )
        );
    }

    /**
     * Get the Fully Qualified Namespace root for Traits for the specified Entity
     *
     * @param string $entityFqn
     *
     * @return string
     */
    public function getTraitsNamespaceForEntity(
        string $entityFqn
    ): string {
        $traitsNamespace = $this->getProjectNamespaceRootFromEntityFqn($entityFqn)
                           . AbstractGenerator::ENTITY_RELATIONS_NAMESPACE
                           . '\\' . $this->getEntitySubNamespace($entityFqn)
                           . '\\Traits';

        return $traitsNamespace;
    }

    /**
     * Use the fully qualified name of two Entities to calculate the Project Namespace Root
     *
     * - note: this assumes a single namespace level for entities, eg `Entities`
     *
     * @param string $entityFqn
     *
     * @return string
     */
    public function getProjectNamespaceRootFromEntityFqn(string $entityFqn): string
    {
        return $this->tidy(
            substr(
                $entityFqn,
                0,
                strrpos(
                    $entityFqn,
                    '\\' . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\'
                )
            )
        );
    }

    /**
     * Get the Fully Qualified Namespace for the "HasEntities" interface for the specified Entity
     *
     * @param string $entityFqn
     *
     * @return string
     */
    public function getHasPluralInterfaceFqnForEntity(
        string $entityFqn
    ): string {
        $interfaceNamespace = $this->getInterfacesNamespaceForEntity($entityFqn);

        return $interfaceNamespace . '\\Has' . ucfirst($entityFqn::getDoctrineStaticMeta()->getPlural()) . 'Interface';
    }

    /**
     * Get the Fully Qualified Namespace root for Interfaces for the specified Entity
     *
     * @param string $entityFqn
     *
     * @return string
     */
    public function getInterfacesNamespaceForEntity(
        string $entityFqn
    ): string {
        $interfacesNamespace = $this->getProjectNamespaceRootFromEntityFqn($entityFqn)
                               . AbstractGenerator::ENTITY_RELATIONS_NAMESPACE
                               . '\\' . $this->getEntitySubNamespace($entityFqn)
                               . '\\Interfaces';

        return $this->tidy($interfacesNamespace);
    }

    /**
     * Get the Fully Qualified Namespace for the "HasEntity" interface for the specified Entity
     *
     * @param string $entityFqn
     *
     * @return string
     * @throws DoctrineStaticMetaException
     */
    public function getHasSingularInterfaceFqnForEntity(
        string $entityFqn
    ): string {
        try {
            $interfaceNamespace = $this->getInterfacesNamespaceForEntity($entityFqn);

            return $interfaceNamespace . '\\Has' . ucfirst($entityFqn::getDoctrineStaticMeta()->getSingular())
                   . 'Interface';
        } catch (Exception $e) {
            throw new DoctrineStaticMetaException(
                'Exception in ' . __METHOD__ . ': ' . $e->getMessage(),
                $e->getCode(),
                $e
            );
        }
    }

    /**
     * Get the Fully Qualified Namespace for the Relation Trait for a specific Entity and hasType
     *
     * @param string      $hasType
     * @param string      $ownedEntityFqn
     * @param string|null $projectRootNamespace
     * @param string      $srcFolder
     *
     * @return string
     * @throws DoctrineStaticMetaException
     */
    public function getOwningTraitFqn(
        string $hasType,
        string $ownedEntityFqn,
        ?string $projectRootNamespace = null,
        string $srcFolder = AbstractCommand::DEFAULT_SRC_SUBFOLDER
    ): string {
        try {
            $ownedHasName = $this->getOwnedHasName($hasType, $ownedEntityFqn, $srcFolder, $projectRootNamespace);
            if (null === $projectRootNamespace) {
                $projectRootNamespace = $this->getProjectRootNamespaceFromComposerJson($srcFolder);
            }
            list($ownedClassName, , $ownedSubDirectories) = $this->parseFullyQualifiedName(
                $ownedEntityFqn,
                $srcFolder,
                $projectRootNamespace
            );
            $traitSubDirectories = array_slice($ownedSubDirectories, 2);
            $owningTraitFqn      = $this->getOwningRelationsRootFqn(
                $projectRootNamespace,
                $traitSubDirectories
            );
            $required            = \ts\stringContains($hasType, RelationsGenerator::PREFIX_REQUIRED)
                ? RelationsGenerator::PREFIX_REQUIRED
                : '';
            $owningTraitFqn      .= $ownedClassName . '\\Traits\\Has' . $required . $ownedHasName
                                    . '\\' . $this->getBaseHasTypeTraitFqn($ownedHasName, $hasType);

            return $this->tidy($owningTraitFqn);
        } catch (Exception $e) {
            throw new DoctrineStaticMetaException(
                'Exception in ' . __METHOD__ . ': ' . $e->getMessage(),
                $e->getCode(),
                $e
            );
        }
    }

    /**
     * Based on the $hasType, we calculate exactly what type of `Has` we have
     *
     * @param string $hasType
     * @param string $ownedEntityFqn
     * @param string $srcOrTestSubFolder
     *
     * @param string $projectRootNamespace
     *
     * @return string
     * @SuppressWarnings(PHPMD.StaticAccess)
     * @throws DoctrineStaticMetaException
     */
    public function getOwnedHasName(
        string $hasType,
        string $ownedEntityFqn,
        string $srcOrTestSubFolder,
        string $projectRootNamespace
    ): string {
        $parsedFqn = $this->parseFullyQualifiedName(
            $ownedEntityFqn,
            $srcOrTestSubFolder,
            $projectRootNamespace
        );

        $subDirectories = $parsedFqn[2];

        if (
            in_array(
                $hasType,
                RelationsGenerator::HAS_TYPES_PLURAL,
                true
            )
        ) {
            return $this->getPluralNamespacedName($ownedEntityFqn, $subDirectories);
        }

        return $this->getSingularNamespacedName($ownedEntityFqn, $subDirectories);
    }

    /**
     * From the fully qualified name, parse out:
     *  - class name,
     *  - namespace
     *  - the namespace parts not including the project root namespace
     *
     * @param string      $fqn
     *
     * @param string      $srcOrTestSubFolder eg 'src' or 'test'
     *
     * @param string|null $projectRootNamespace
     *
     * @return array [$className,$namespace,$subDirectories]
     * @throws DoctrineStaticMetaException
     */
    public function parseFullyQualifiedName(
        string $fqn,
        string $srcOrTestSubFolder = AbstractCommand::DEFAULT_SRC_SUBFOLDER,
        string $projectRootNamespace = null
    ): array {
        try {
            $fqn = $this->root($fqn);
            if (null === $projectRootNamespace) {
                $projectRootNamespace = $this->getProjectRootNamespaceFromComposerJson($srcOrTestSubFolder);
            }
            $projectRootNamespace = $this->root($projectRootNamespace);
            if (false === \ts\stringContains($fqn, $projectRootNamespace)) {
                throw new DoctrineStaticMetaException(
                    'The $fqn [' . $fqn . '] does not contain the project root namespace'
                    . ' [' . $projectRootNamespace . '] - are you sure it is the correct FQN?'
                );
            }
            $fqnParts       = explode('\\', $fqn);
            $className      = array_pop($fqnParts);
            $namespace      = implode('\\', $fqnParts);
            $rootParts      = explode('\\', $projectRootNamespace);
            $subDirectories = [];
            foreach ($fqnParts as $k => $fqnPart) {
                if (isset($rootParts[$k]) && $rootParts[$k] === $fqnPart) {
                    continue;
                }
                $subDirectories[] = $fqnPart;
            }
            array_unshift($subDirectories, $srcOrTestSubFolder);

            return [
                $className,
                $this->root($namespace),
                $subDirectories,
            ];
        } catch (Exception $e) {
            throw new DoctrineStaticMetaException(
                'Exception in ' . __METHOD__ . ': ' . $e->getMessage(),
                $e->getCode(),
                $e
            );
        }
    }

    /**
     * Generate a tidy root namespace without a leading \
     *
     * @param string $namespace
     *
     * @return string
     */
    public function root(string $namespace): string
    {
        return $this->tidy(ltrim($namespace, '\\'));
    }

    /**
     * Read src autoloader from composer json
     *
     * @param string $dirForNamespace
     *
     * @return string
     * @throws DoctrineStaticMetaException
     * @SuppressWarnings(PHPMD.StaticAccess)
     */
    public function getProjectRootNamespaceFromComposerJson(
        string $dirForNamespace = 'src'
    ): string {
        try {
            $dirForNamespace = trim($dirForNamespace, '/');
            $jsonPath        = Config::getProjectRootDirectory() . '/composer.json';
            $json            = json_decode(\ts\file_get_contents($jsonPath), true);
            if (JSON_ERROR_NONE !== json_last_error()) {
                throw new RuntimeException(
                    'Error decoding json from path ' . $jsonPath . ' , ' . json_last_error_msg()
                );
            }
            /**
             * @var string[][][][] $json
             */
            if (isset($json['autoload']['psr-4'])) {
                foreach ($json['autoload']['psr-4'] as $namespace => $dirs) {
                    if (!is_array($dirs)) {
                        $dirs = [$dirs];
                    }
                    foreach ($dirs as $dir) {
                        $dir = trim($dir, '/');
                        if ($dir === $dirForNamespace) {
                            return $this->tidy(rtrim($namespace, '\\'));
                        }
                    }
                }
            }
        } catch (Exception $e) {
            throw new DoctrineStaticMetaException(
                'Exception in ' . __METHOD__ . ': ' . $e->getMessage(),
                $e->getCode(),
                $e
            );
        }
        throw new DoctrineStaticMetaException('Failed to find psr-4 namespace root');
    }

    /**
     * @param string $entityFqn
     * @param array  $subDirectories
     *
     * @return string
     * @SuppressWarnings(PHPMD.StaticAccess)
     */
    public function getPluralNamespacedName(string $entityFqn, array $subDirectories): string
    {
        $plural = ucfirst(MappingHelper::getPluralForFqn($entityFqn));

        return $this->getNamespacedName($plural, $subDirectories);
    }

    /**
     * @param string $entityName
     * @param array  $subDirectories
     *
     * @return string
     */
    public function getNamespacedName(string $entityName, array $subDirectories): string
    {
        $noEntitiesDirectory = array_slice($subDirectories, 2);
        $namespacedName      = array_merge($noEntitiesDirectory, [$entityName]);

        return ucfirst(implode('', $namespacedName));
    }

    /**
     * @param string $entityFqn
     * @param array  $subDirectories
     *
     * @return string
     * @SuppressWarnings(PHPMD.StaticAccess)
     */
    public function getSingularNamespacedName(string $entityFqn, array $subDirectories): string
    {
        $singular = ucfirst(MappingHelper::getSingularForFqn($entityFqn));

        return $this->getNamespacedName($singular, $subDirectories);
    }

    /**
     * Get the Namespace root for Entity Relations
     *
     * @param string $projectRootNamespace
     * @param array  $subDirectories
     *
     * @return string
     */
    public function getOwningRelationsRootFqn(
        string $projectRootNamespace,
        array $subDirectories
    ): string {
        $relationsRootFqn = $projectRootNamespace
                            . AbstractGenerator::ENTITY_RELATIONS_NAMESPACE . '\\';
        if (count($subDirectories) > 0) {
            $relationsRootFqn .= implode('\\', $subDirectories) . '\\';
        }

        return $this->tidy($relationsRootFqn);
    }

    /**
     * Normalise a has type, removing prefixes that are not required
     *
     * Inverse hasTypes use the standard template without the prefix
     * The exclusion ot this are the ManyToMany and OneToOne relations
     *
     * @param string $ownedHasName
     * @param string $hasType
     *
     * @return string
     */
    public function getBaseHasTypeTraitFqn(
        string $ownedHasName,
        string $hasType
    ): string {
        $required = \ts\stringContains($hasType, RelationsGenerator::PREFIX_REQUIRED)
            ? RelationsGenerator::PREFIX_REQUIRED
            : '';

        $hasType = str_replace(RelationsGenerator::PREFIX_REQUIRED, '', $hasType);
        foreach (
            [
                     RelationsGenerator::INTERNAL_TYPE_MANY_TO_MANY,
                     RelationsGenerator::INTERNAL_TYPE_ONE_TO_ONE,
                 ] as $noStrip
        ) {
            if (\ts\stringContains($hasType, $noStrip)) {
                return 'Has' . $required . $ownedHasName . $hasType;
            }
        }

        foreach (
            [
                     RelationsGenerator::INTERNAL_TYPE_ONE_TO_MANY,
                     RelationsGenerator::INTERNAL_TYPE_MANY_TO_ONE,
                 ] as $stripAll
        ) {
            if (\ts\stringContains($hasType, $stripAll)) {
                return str_replace(
                    [
                        RelationsGenerator::PREFIX_OWNING,
                        RelationsGenerator::PREFIX_INVERSE,
                    ],
                    '',
                    'Has' . $required . $ownedHasName . $hasType
                );
            }
        }

        return str_replace(
            [
                RelationsGenerator::PREFIX_INVERSE,
            ],
            '',
            'Has' . $required . $ownedHasName . $hasType
        );
    }

    public function getFactoryFqnFromEntityFqn(string $entityFqn): string
    {
        return $this->tidy(
            str_replace(
                '\\' . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\',
                '\\' . AbstractGenerator::ENTITY_FACTORIES_NAMESPACE . '\\',
                $entityFqn
            ) . 'Factory'
        );
    }

    public function getDtoFactoryFqnFromEntityFqn(string $entityFqn): string
    {
        return $this->tidy(
            str_replace(
                '\\' . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\',
                '\\' . AbstractGenerator::ENTITY_FACTORIES_NAMESPACE . '\\',
                $entityFqn
            ) . 'DtoFactory'
        );
    }

    public function getRepositoryqnFromEntityFqn(string $entityFqn): string
    {
        return $this->tidy(
            str_replace(
                '\\' . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\',
                '\\' . AbstractGenerator::ENTITY_REPOSITORIES_NAMESPACE . '\\',
                $entityFqn
            ) . 'Repository'
        );
    }

    /**
     * @param string $ownedEntityFqn
     * @param string $srcOrTestSubFolder
     * @param string $projectRootNamespace
     *
     * @return string
     * @throws DoctrineStaticMetaException
     */
    public function getReciprocatedHasName(
        string $ownedEntityFqn,
        string $srcOrTestSubFolder,
        string $projectRootNamespace
    ): string {
        $parsedFqn = $this->parseFullyQualifiedName(
            $ownedEntityFqn,
            $srcOrTestSubFolder,
            $projectRootNamespace
        );

        $subDirectories = $parsedFqn[2];

        return $this->getSingularNamespacedName($ownedEntityFqn, $subDirectories);
    }

    /**
     * Get the Fully Qualified Namespace for the Relation Interface for a specific Entity and hasType
     *
     * @param string      $hasType
     * @param string      $ownedEntityFqn
     * @param string|null $projectRootNamespace
     * @param string      $srcFolder
     *
     * @return string
     * @throws DoctrineStaticMetaException
     */
    public function getOwningInterfaceFqn(
        string $hasType,
        string $ownedEntityFqn,
        string $projectRootNamespace = null,
        string $srcFolder = AbstractCommand::DEFAULT_SRC_SUBFOLDER
    ): string {
        try {
            $ownedHasName = $this->getOwnedHasName($hasType, $ownedEntityFqn, $srcFolder, $projectRootNamespace);
            if (null === $projectRootNamespace) {
                $projectRootNamespace = $this->getProjectRootNamespaceFromComposerJson($srcFolder);
            }
            list($ownedClassName, , $ownedSubDirectories) = $this->parseFullyQualifiedName(
                $ownedEntityFqn,
                $srcFolder,
                $projectRootNamespace
            );
            $interfaceSubDirectories = array_slice($ownedSubDirectories, 2);
            $owningInterfaceFqn      = $this->getOwningRelationsRootFqn(
                $projectRootNamespace,
                $interfaceSubDirectories
            );
            $required                = \ts\stringContains($hasType, RelationsGenerator::PREFIX_REQUIRED)
                ? 'Required'
                : '';
            $owningInterfaceFqn      .= '\\' .
                                        $ownedClassName .
                                        '\\Interfaces\\Has' .
                                        $required .
                                        $ownedHasName .
                                        'Interface';

            return $this->tidy($owningInterfaceFqn);
        } catch (Exception $e) {
            throw new DoctrineStaticMetaException(
                'Exception in ' . __METHOD__ . ': ' . $e->getMessage(),
                $e->getCode(),
                $e
            );
        }
    }

    public function getEntityInterfaceFromEntityFqn(string $entityFqn): string
    {
        return str_replace(
            '\\Entities\\',
            '\\Entity\\Interfaces\\',
            $entityFqn
        ) . 'Interface';
    }

    public function getEntityFqnFromEntityInterfaceFqn(string $entityInterfaceFqn): string
    {
        return substr(
            str_replace(
                '\\Entity\\Interfaces\\',
                '\\Entities\\',
                $entityInterfaceFqn
            ),
            0,
            -strlen('Interface')
        );
    }

    public function getEntityFqnFromEntityFactoryFqn(string $entityFactoryFqn): string
    {
        return substr(
            str_replace(
                '\\Entity\\Factories\\',
                '\\Entities\\',
                $entityFactoryFqn
            ),
            0,
            -strlen('Factory')
        );
    }

    public function getEntityFqnFromEntityDtoFactoryFqn(string $entityDtoFactoryFqn): string
    {
        return substr(
            str_replace(
                '\\Entity\\Factories\\',
                '\\Entities\\',
                $entityDtoFactoryFqn
            ),
            0,
            -strlen('DtoFactory')
        );
    }

    public function getEntityDtoFqnFromEntityFqn(string $entityFqn): string
    {
        return str_replace(
            '\\Entities\\',
            '\\Entity\\DataTransferObjects\\',
            $entityFqn
        ) . 'Dto';
    }

    /**
     * @param string $entityDtoFqn
     *
     * @return string
     * @deprecated please use the static method on the DTO directly
     *
     */
    public function getEntityFqnFromEntityDtoFqn(string $entityDtoFqn): string
    {
        return $entityDtoFqn::getEntityFqn();
    }

    public function getEntityFqnFromEntityRepositoryFqn(string $entityRepositoryFqn): string
    {
        return substr(
            str_replace(
                '\\Entity\\Repositories\\',
                '\\Entities\\',
                $entityRepositoryFqn
            ),
            0,
            -strlen('Repository')
        );
    }

    public function getEntityFqnFromEntitySaverFqn(string $entitySaverFqn): string
    {
        return substr(
            str_replace(
                '\\Entity\\Savers\\',
                '\\Entities\\',
                $entitySaverFqn
            ),
            0,
            -strlen('Saver')
        );
    }

    public function getEntitySaverFqnFromEntityFqn(string $entityFqn): string
    {
        return str_replace(
            '\\Entities\\',
            '\\Entity\\Savers\\',
            $entityFqn
        ) . 'Saver';
    }

    public function getEntityFqnFromEntityUpserterFqn(string $entityUpserterFqn): string
    {
        return substr(
            str_replace(
                '\\Entity\\Savers\\',
                '\\Entities\\',
                $entityUpserterFqn
            ),
            0,
            -strlen('Upserter')
        );
    }

    public function getEntityUpserterFqnFromEntityFqn(string $entityFqn): string
    {
        return str_replace(
            '\\Entities\\',
            '\\Entity\\Savers\\',
            $entityFqn
        ) . 'Upserter';
    }

    public function getEntityFqnFromEntityUnitOfWorkHelperFqn(string $entityUnitofWorkHelperFqn): string
    {
        return substr(
            str_replace(
                '\\Entity\\Savers\\',
                '\\Entities\\',
                $entityUnitofWorkHelperFqn
            ),
            0,
            -strlen('UnitOfWorkHelper')
        );
    }

    public function getEntityUnitOfWorkHelperFqnFromEntityFqn(string $entityFqn): string
    {
        return str_replace(
            '\\Entities\\',
            '\\Entity\\Savers\\',
            $entityFqn
        ) . 'UnitOfWorkHelper';
    }

    public function getEntityFqnFromEntityTestFqn(string $entityTestFqn): string
    {
        return substr(
            $entityTestFqn,
            0,
            -strlen('Test')
        );
    }

    public function getEntityTestFqnFromEntityFqn(string $entityFqn): string
    {
        return $entityFqn . 'Test';
    }
}