edmondscommerce/doctrine-static-meta

View on GitHub
src/CodeGeneration/Generator/RelationsGenerator.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php

declare(strict_types=1);

namespace EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator;

use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\Relations\GenerateRelationCodeForEntity;
use EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException;
use Exception;
use Generator;
use gossi\codegen\model\PhpClass;
use gossi\codegen\model\PhpInterface;
use gossi\codegen\model\PhpTrait;
use InvalidArgumentException;
use PhpParser\Error;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionException;
use RuntimeException;
use ts\Reflection\ReflectionClass;

use function file_exists;
use function in_array;
use function preg_replace;
use function print_r;
use function realpath;
use function str_replace;

/**
 * Class RelationsGenerator
 *
 * @package EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class RelationsGenerator extends AbstractGenerator
{
    public const PREFIX_OWNING         = 'Owning';
    public const PREFIX_INVERSE        = 'Inverse';
    public const PREFIX_UNIDIRECTIONAL = 'Unidirectional';
    public const PREFIX_REQUIRED       = 'Required';


    /*******************************************************************************************************************
     * OneToOne - One instance of the current Entity refers to One instance of the referred Entity.
     */
    public const INTERNAL_TYPE_ONE_TO_ONE = 'OneToOne';

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntity/HasTemplateEntityOwningOneToOne.php
     */
    public const HAS_ONE_TO_ONE = self::PREFIX_OWNING . self::INTERNAL_TYPE_ONE_TO_ONE;

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasRequiredTemplateEntity/HasRequiredTemplateEntityOwningOneToOne.php
     */
    public const HAS_REQUIRED_ONE_TO_ONE = self::PREFIX_REQUIRED . self::PREFIX_OWNING . self::INTERNAL_TYPE_ONE_TO_ONE;

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntity/HasTemplateEntityInverseOneToOne.php
     */
    public const HAS_INVERSE_ONE_TO_ONE = self::PREFIX_INVERSE . self::INTERNAL_TYPE_ONE_TO_ONE;

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasRequiredTemplateEntity/HasRequiredTemplateEntityInverseOneToOne.php
     */
    public const HAS_REQUIRED_INVERSE_ONE_TO_ONE = self::PREFIX_REQUIRED .
                                                   self::PREFIX_INVERSE .
                                                   self::INTERNAL_TYPE_ONE_TO_ONE;

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntity/HasTemplateEntityUnidrectionalOneToOne.php
     */
    public const HAS_UNIDIRECTIONAL_ONE_TO_ONE = self::PREFIX_UNIDIRECTIONAL . self::INTERNAL_TYPE_ONE_TO_ONE;

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasRequiredTemplateEntity/HasRequiredTemplateEntityUnidrectionalOneToOne.php
     */
    public const HAS_REQUIRED_UNIDIRECTIONAL_ONE_TO_ONE = self::PREFIX_REQUIRED .
                                                          self::PREFIX_UNIDIRECTIONAL .
                                                          self::INTERNAL_TYPE_ONE_TO_ONE;

    /*******************************************************************************************************************
     * OneToMany - One instance of the current Entity has Many instances (references) to the referred Entity.
     */
    public const INTERNAL_TYPE_ONE_TO_MANY = 'OneToMany';

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntities/HasTemplateEntitiesOneToMany.php
     */
    public const HAS_ONE_TO_MANY = self::INTERNAL_TYPE_ONE_TO_MANY;

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasRequiredTemplateEntities/HasRequiredTemplateEntitiesOneToMany.php
     */
    public const HAS_REQUIRED_ONE_TO_MANY = self::PREFIX_REQUIRED . self::INTERNAL_TYPE_ONE_TO_MANY;

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntities/HasTemplateEntitiesOneToMany.php
     */
    public const HAS_UNIDIRECTIONAL_ONE_TO_MANY = self::PREFIX_UNIDIRECTIONAL . self::INTERNAL_TYPE_ONE_TO_MANY;

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasRequiredTemplateEntities/HasRequiredTemplateEntitiesOneToMany.php
     */
    public const HAS_REQUIRED_UNIDIRECTIONAL_ONE_TO_MANY = self::PREFIX_REQUIRED .
                                                           self::PREFIX_UNIDIRECTIONAL .
                                                           self::INTERNAL_TYPE_ONE_TO_MANY;

    /*******************************************************************************************************************
     * ManyToOne - Many instances of the current Entity refer to One instance of the referred Entity.
     */
    public const INTERNAL_TYPE_MANY_TO_ONE = 'ManyToOne';

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntity/HasTemplateEntityManyToOne.php
     */
    public const HAS_MANY_TO_ONE = self::INTERNAL_TYPE_MANY_TO_ONE;

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasRequiredTemplateEntity/HasRequiredTemplateEntityManyToOne.php
     */
    public const HAS_REQUIRED_MANY_TO_ONE = self::PREFIX_REQUIRED . self::INTERNAL_TYPE_MANY_TO_ONE;

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntity/HasTemplateEntityManyToOne.php
     */
    public const HAS_UNIDIRECTIONAL_MANY_TO_ONE = self::PREFIX_UNIDIRECTIONAL . self::INTERNAL_TYPE_MANY_TO_ONE;

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasRequiredTemplateEntity/HasRequiredTemplateEntityManyToOne.php
     */
    public const HAS_REQUIRED_UNIDIRECTIONAL_MANY_TO_ONE = self::PREFIX_REQUIRED .
                                                           self::PREFIX_UNIDIRECTIONAL .
                                                           self::INTERNAL_TYPE_MANY_TO_ONE;


    /*******************************************************************************************************************
     * ManyToMany - Many instances of the current Entity refer to Many instance of the referred Entity.
     */
    public const INTERNAL_TYPE_MANY_TO_MANY = 'ManyToMany';

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntities/HasTemplateEntitiesOwningManyToMany.php
     */
    public const HAS_MANY_TO_MANY = self::PREFIX_OWNING . self::INTERNAL_TYPE_MANY_TO_MANY;

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasRequiredTemplateEntities/HasRequiredTemplateEntitiesOwningManyToMany.php
     */
    public const HAS_REQUIRED_MANY_TO_MANY = self::PREFIX_REQUIRED .
                                             self::PREFIX_OWNING .
                                             self::INTERNAL_TYPE_MANY_TO_MANY;
    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntities/HasTemplateEntitiesInverseManyToMany.php
     */
    public const HAS_INVERSE_MANY_TO_MANY = self::PREFIX_INVERSE . self::INTERNAL_TYPE_MANY_TO_MANY;

    /**
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasRequiredTemplateEntities/HasRequiredTemplateEntitiesInverseManyToMany.php
     */
    public const HAS_REQUIRED_INVERSE_MANY_TO_MANY = self::PREFIX_REQUIRED .
                                                     self::PREFIX_INVERSE .
                                                     self::INTERNAL_TYPE_MANY_TO_MANY;


    /**
     * The full list of possible relation types
     */
    public const HAS_TYPES = [
        self::HAS_ONE_TO_ONE,
        self::HAS_INVERSE_ONE_TO_ONE,
        self::HAS_UNIDIRECTIONAL_ONE_TO_ONE,
        self::HAS_ONE_TO_MANY,
        self::HAS_UNIDIRECTIONAL_ONE_TO_MANY,
        self::HAS_MANY_TO_ONE,
        self::HAS_UNIDIRECTIONAL_MANY_TO_ONE,
        self::HAS_MANY_TO_MANY,
        self::HAS_INVERSE_MANY_TO_MANY,

        self::HAS_REQUIRED_ONE_TO_ONE,
        self::HAS_REQUIRED_INVERSE_ONE_TO_ONE,
        self::HAS_REQUIRED_UNIDIRECTIONAL_ONE_TO_ONE,
        self::HAS_REQUIRED_ONE_TO_MANY,
        self::HAS_REQUIRED_UNIDIRECTIONAL_ONE_TO_MANY,
        self::HAS_REQUIRED_MANY_TO_ONE,
        self::HAS_REQUIRED_UNIDIRECTIONAL_MANY_TO_ONE,
        self::HAS_REQUIRED_MANY_TO_MANY,
        self::HAS_REQUIRED_INVERSE_MANY_TO_MANY,
    ];

    /**
     * Of the full list, which ones will be automatically reciprocated in the generated code
     */
    public const HAS_TYPES_RECIPROCATED = [
        self::HAS_ONE_TO_ONE,
        self::HAS_INVERSE_ONE_TO_ONE,
        self::HAS_ONE_TO_MANY,
        self::HAS_MANY_TO_ONE,
        self::HAS_MANY_TO_MANY,
        self::HAS_INVERSE_MANY_TO_MANY,

        self::HAS_REQUIRED_ONE_TO_ONE,
        self::HAS_REQUIRED_INVERSE_ONE_TO_ONE,
        self::HAS_REQUIRED_ONE_TO_MANY,
        self::HAS_REQUIRED_MANY_TO_ONE,
        self::HAS_REQUIRED_MANY_TO_MANY,
        self::HAS_REQUIRED_INVERSE_MANY_TO_MANY,
    ];

    /**
     *Of the full list, which ones are unidirectional (i.e not reciprocated)
     */
    public const HAS_TYPES_UNIDIRECTIONAL = [
        self::HAS_UNIDIRECTIONAL_MANY_TO_ONE,
        self::HAS_UNIDIRECTIONAL_ONE_TO_MANY,
        self::HAS_UNIDIRECTIONAL_ONE_TO_ONE,

        self::HAS_REQUIRED_UNIDIRECTIONAL_MANY_TO_ONE,
        self::HAS_REQUIRED_UNIDIRECTIONAL_ONE_TO_MANY,
        self::HAS_REQUIRED_UNIDIRECTIONAL_ONE_TO_ONE,
    ];

    /**
     * Of the full list, which ones are a plural relationship, i.e they have multiple of the related entity
     */
    public const HAS_TYPES_PLURAL = [
        self::HAS_MANY_TO_MANY,
        self::HAS_INVERSE_MANY_TO_MANY,
        self::HAS_ONE_TO_MANY,
        self::HAS_UNIDIRECTIONAL_ONE_TO_MANY,

        self::HAS_REQUIRED_MANY_TO_MANY,
        self::HAS_REQUIRED_INVERSE_MANY_TO_MANY,
        self::HAS_REQUIRED_ONE_TO_MANY,
        self::HAS_REQUIRED_UNIDIRECTIONAL_ONE_TO_MANY,
    ];

    /**
     * Set a relationship from one Entity to Another Entity.
     *
     * Also used internally to set the reciprocal side. Uses an undocumented 4th bool parameter to kill recursion.
     *
     * @param string $owningEntityFqn
     * @param string $hasType
     * @param string $ownedEntityFqn
     * @param bool   $requiredReciprocation
     *
     * You should never pass in this parameter, it is only used internally
     * @param bool   $internalUseOnly
     *
     * @throws DoctrineStaticMetaException
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
     */
    public function setEntityHasRelationToEntity(
        string $owningEntityFqn,
        string $hasType,
        string $ownedEntityFqn,
        bool $requiredReciprocation = false,
        bool $internalUseOnly = true
    ): void {
        $reciprocate = $internalUseOnly;
        try {
            $this->validateHasType($hasType);
            [
                $owningTraitPath,
                $owningInterfacePath,
                $reciprocatingInterfacePath,
            ] = $this->getPathsForOwningTraitsAndInterfaces(
                $hasType,
                $ownedEntityFqn
            );
            list($owningClass, , $owningClassSubDirs) = $this->parseFullyQualifiedName($owningEntityFqn);
            $owningClassPath = $this->pathHelper->getPathFromNameAndSubDirs(
                $this->pathToProjectRoot,
                $owningClass,
                $owningClassSubDirs
            );
            $this->useRelationTraitInClass($owningClassPath, $owningTraitPath);
            $this->useRelationInterfaceInEntityInterface($owningClassPath, $owningInterfacePath);
            if (in_array($hasType, self::HAS_TYPES_RECIPROCATED, true)) {
                $this->useRelationInterfaceInEntityInterface($owningClassPath, $reciprocatingInterfacePath);
            }
            if (true === $reciprocate && in_array($hasType, self::HAS_TYPES_RECIPROCATED, true)) {
                $inverseType = $this->getInverseHasType($hasType);
                $inverseType = $this->updateHasTypeForPossibleRequired($inverseType, $requiredReciprocation);
                $this->setEntityHasRelationToEntity(
                    $ownedEntityFqn,
                    $inverseType,
                    $owningEntityFqn,
                    /**
                     * Setting required reciprocation to false,
                     * actually it is irrelevant because reciprocation is disabled
                     */
                    false,
                    false
                );
            }
        } catch (Exception $e) {
            throw new DoctrineStaticMetaException(
                'Exception in ' . __METHOD__ . ': ' . $e->getMessage(),
                $e->getCode(),
                $e
            );
        }
    }

    /**
     * @param string $hasType
     *
     * @throws InvalidArgumentException
     */
    protected function validateHasType(string $hasType): void
    {
        if (!in_array($hasType, static::HAS_TYPES, true)) {
            throw new InvalidArgumentException(
                'Invalid $hasType ' . $hasType . ', must be one of: '
                . print_r(static::HAS_TYPES, true)
            );
        }
    }

    /**
     * Get the absolute paths for the owning traits and interfaces for the specified relation type
     * Will ensure that the files exists
     *
     * @param string $hasType
     * @param string $ownedEntityFqn
     *
     * @return array [
     *  $owningTraitPath,
     *  $owningInterfacePath,
     *  $reciprocatingInterfacePath
     * ]
     * @throws DoctrineStaticMetaException
     * @SuppressWarnings(PHPMD.StaticAccess)
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
     */
    protected function getPathsForOwningTraitsAndInterfaces(
        string $hasType,
        string $ownedEntityFqn
    ): array {
        try {
            $ownedHasName        = $this->namespaceHelper->getOwnedHasName(
                $hasType,
                $ownedEntityFqn,
                $this->srcSubFolderName,
                $this->projectRootNamespace
            );
            $reciprocatedHasName = $this->namespaceHelper->getReciprocatedHasName(
                $ownedEntityFqn,
                $this->srcSubFolderName,
                $this->projectRootNamespace
            );
            $owningTraitFqn      = $this->getOwningTraitFqn($hasType, $ownedEntityFqn);
            list($traitName, , $traitSubDirsNoEntities) = $this->parseFullyQualifiedName($owningTraitFqn);
            $owningTraitPath = $this->pathHelper->getPathFromNameAndSubDirs(
                $this->pathToProjectRoot,
                $traitName,
                $traitSubDirsNoEntities
            );
            if (!file_exists($owningTraitPath)) {
                $this->generateRelationCodeForEntity($ownedEntityFqn);
            }
            $owningInterfaceFqn = $this->getOwningInterfaceFqn($hasType, $ownedEntityFqn);
            list($interfaceName, , $interfaceSubDirsNoEntities) = $this->parseFullyQualifiedName($owningInterfaceFqn);
            $owningInterfacePath        = $this->pathHelper->getPathFromNameAndSubDirs(
                $this->pathToProjectRoot,
                $interfaceName,
                $interfaceSubDirsNoEntities
            );
            $reciprocatingInterfacePath = preg_replace(
                '%Has(Required|)' . $ownedHasName . '%',
                'Reciprocates' . $reciprocatedHasName,
                $owningInterfacePath
            );

            return [
                $owningTraitPath,
                $owningInterfacePath,
                $reciprocatingInterfacePath,
            ];
        } catch (Exception $e) {
            throw new DoctrineStaticMetaException(
                'Exception in ' . __METHOD__ . ': ' . $e->getMessage(),
                $e->getCode(),
                $e
            );
        }
    }

    /**
     * @param string $hasType
     * @param string $ownedEntityFqn
     *
     * @return string
     * @throws DoctrineStaticMetaException
     */
    public function getOwningTraitFqn(string $hasType, string $ownedEntityFqn): string
    {
        return $this->namespaceHelper->getOwningTraitFqn(
            $hasType,
            $ownedEntityFqn,
            $this->projectRootNamespace,
            $this->srcSubFolderName
        );
    }

    /**
     * Generate the relation traits for specified Entity
     *
     * This works by copying the template traits folder over and then updating the file contents, name and path
     *
     * @param string $entityFqn Fully Qualified Name of Entity
     *
     * @throws DoctrineStaticMetaException
     * @SuppressWarnings(PHPMD.StaticAccess)
     */
    public function generateRelationCodeForEntity(string $entityFqn): void
    {
        $invokable = new GenerateRelationCodeForEntity(
            $entityFqn,
            $this->pathToProjectRoot,
            $this->projectRootNamespace,
            $this->srcSubFolderName,
            $this->namespaceHelper,
            $this->pathHelper,
            $this->findAndReplaceHelper
        );
        $invokable($this->getRelativePathRelationsGenerator());
    }

    /**
     * Generator that yields relative paths of all the files in the relations template path and the SplFileInfo objects
     *
     * Use a PHP Generator to iterate over a recursive iterator iterator and then yield:
     * - key: string $relativePath
     * - value: \SplFileInfo $fileInfo
     *
     * The `finally` step unsets the recursiveIterator once everything is done
     *
     * @return Generator
     */
    public function getRelativePathRelationsGenerator(): Generator
    {
        try {
            $recursiveIterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator(
                    realpath(AbstractGenerator::RELATIONS_TEMPLATE_PATH),
                    RecursiveDirectoryIterator::SKIP_DOTS
                ),
                RecursiveIteratorIterator::SELF_FIRST
            );
            foreach ($recursiveIterator as $path => $fileInfo) {
                $relativePath = rtrim(
                    $this->getFilesystem()->makePathRelative(
                        $path,
                        realpath(AbstractGenerator::RELATIONS_TEMPLATE_PATH)
                    ),
                    '/'
                );
                yield $relativePath => $fileInfo;
            }
        } finally {
            $recursiveIterator = null;
            unset($recursiveIterator);
        }
    }

    /**
     * @param string $hasType
     * @param string $ownedEntityFqn
     *
     * @return string
     * @throws DoctrineStaticMetaException
     */
    public function getOwningInterfaceFqn(string $hasType, string $ownedEntityFqn): string
    {
        return $this->namespaceHelper->getOwningInterfaceFqn(
            $hasType,
            $ownedEntityFqn,
            $this->projectRootNamespace,
            $this->srcSubFolderName
        );
    }

    /**
     * Add the specified trait to the specified class
     *
     * @param string $classPath
     * @param string $traitPath
     *
     * @throws DoctrineStaticMetaException
     * @SuppressWarnings(PHPMD.StaticAccess)
     */
    protected function useRelationTraitInClass(string $classPath, string $traitPath): void
    {
        try {
            $class = PhpClass::fromFile($classPath);
        } catch (Error $e) {
            throw new DoctrineStaticMetaException(
                'PHP parsing error when loading class ' . $classPath . ': ' . $e->getMessage(),
                $e->getCode(),
                $e
            );
        }
        try {
            $trait = PhpTrait::fromFile($traitPath);
        } catch (Error $e) {
            throw new DoctrineStaticMetaException(
                'PHP parsing error when loading class ' . $classPath . ': ' . $e->getMessage(),
                $e->getCode(),
                $e
            );
        }
        $class->addTrait($trait);
        $this->codeHelper->generate($class, $classPath);
    }

    /**
     * Add the specified interface to the specified entity interface
     *
     * @param string $classPath
     * @param string $interfacePath
     *
     * @throws ReflectionException
     * @SuppressWarnings(PHPMD.StaticAccess)
     */
    protected function useRelationInterfaceInEntityInterface(string $classPath, string $interfacePath): void
    {
        $entityFqn           = PhpClass::fromFile($classPath)->getQualifiedName();
        $entityInterfaceFqn  = $this->namespaceHelper->getEntityInterfaceFromEntityFqn($entityFqn);
        $entityInterfacePath = (new ReflectionClass($entityInterfaceFqn))->getFileName();
        $entityInterface     = PhpInterface::fromFile($entityInterfacePath);
        $relationInterface   = PhpInterface::fromFile($interfacePath);
        $entityInterface->addInterface($relationInterface);
        $this->codeHelper->generate($entityInterface, $entityInterfacePath);
    }

    /**
     * Get the inverse of a hasType
     *
     * @param string $hasType
     *
     * @return string
     * @throws DoctrineStaticMetaException
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     */
    protected function getInverseHasType(string $hasType): string
    {
        switch ($hasType) {
            case self::HAS_ONE_TO_ONE:
            case self::HAS_REQUIRED_ONE_TO_ONE:
            case self::HAS_MANY_TO_MANY:
            case self::HAS_REQUIRED_MANY_TO_MANY:
                return str_replace(
                    self::PREFIX_OWNING,
                    self::PREFIX_INVERSE,
                    $hasType
                );

            case self::HAS_INVERSE_ONE_TO_ONE:
            case self::HAS_REQUIRED_INVERSE_ONE_TO_ONE:
            case self::HAS_INVERSE_MANY_TO_MANY:
            case self::HAS_REQUIRED_INVERSE_MANY_TO_MANY:
                return str_replace(
                    self::PREFIX_INVERSE,
                    self::PREFIX_OWNING,
                    $hasType
                );

            case self::HAS_MANY_TO_ONE:
                return self::HAS_ONE_TO_MANY;

            case self::HAS_REQUIRED_MANY_TO_ONE:
                return self::HAS_REQUIRED_ONE_TO_MANY;

            case self::HAS_ONE_TO_MANY:
                return self::HAS_MANY_TO_ONE;

            case self::HAS_REQUIRED_ONE_TO_MANY:
                return self::HAS_REQUIRED_MANY_TO_ONE;

            default:
                throw new DoctrineStaticMetaException(
                    'invalid $hasType ' . $hasType . ' when trying to get the inverted relation'
                );
        }
    }

    /**
     * Take a relationship and a possibility of being required and ensure it is set as the correct relationship
     *
     * @param string $relation
     * @param bool   $required
     *
     * @return string
     */
    private function updateHasTypeForPossibleRequired(string $relation, bool $required): string
    {
        $inverseIsRequired = \ts\stringContains($relation, self::PREFIX_REQUIRED);
        if (false === $required) {
            if (false === $inverseIsRequired) {
                return $relation;
            }

            return $this->removeRequiredToRelation($relation);
        }
        if (true === $required) {
            if (true === $inverseIsRequired) {
                return $relation;
            }

            return $this->addRequiredToRelation($relation);
        }
    }

    private function removeRequiredToRelation(string $relation): string
    {
        if (0 !== strpos($relation, self::PREFIX_REQUIRED)) {
            throw new RuntimeException('Trying to remove the Required prefix, but it is not set: ' . $relation);
        }

        return substr($relation, 8);
    }

    private function addRequiredToRelation(string $relation): string
    {
        if (0 === strpos($relation, self::PREFIX_REQUIRED)) {
            throw new RuntimeException('Trying to add the Required prefix, but it is already set: ' . $relation);
        }

        return self::PREFIX_REQUIRED . $relation;
    }
}