src/SDL/Reflection/Builder/Processable/ExtendBuilder.php
<?php
/**
* This file is part of Railt package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Railt\SDL\Reflection\Builder\Processable;
use Phplrt\Ast\NodeInterface;
use Railt\SDL\Base\Dependent\Argument\BaseArgumentsContainer;
use Railt\SDL\Base\Dependent\BaseArgument;
use Railt\SDL\Base\Dependent\BaseField;
use Railt\SDL\Base\Dependent\Field\BaseFieldsContainer;
use Railt\SDL\Base\Invocations\Directive\BaseDirectivesContainer;
use Railt\SDL\Base\Processable\BaseExtend;
use Railt\SDL\Contracts\Definitions\Definition;
use Railt\SDL\Contracts\Dependent\Argument\HasArguments;
use Railt\SDL\Contracts\Dependent\ArgumentDefinition;
use Railt\SDL\Contracts\Dependent\Field\HasFields;
use Railt\SDL\Contracts\Dependent\FieldDefinition;
use Railt\SDL\Contracts\Invocations\Directive\HasDirectives;
use Railt\SDL\Contracts\Invocations\DirectiveInvocation;
use Railt\SDL\Contracts\Processable\ExtendDefinition;
use Railt\SDL\Exceptions\TypeConflictException;
use Railt\SDL\Reflection\Builder\DocumentBuilder;
use Railt\SDL\Reflection\Builder\Process\Compilable;
use Railt\SDL\Reflection\Builder\Process\Compiler;
use Railt\SDL\Reflection\Validation\Inheritance;
/**
* Class ExtendBuilder
*/
class ExtendBuilder extends BaseExtend implements Compilable
{
use Compiler;
/**
* ExtendBuilder constructor.
*
* @param NodeInterface $ast
* @param DocumentBuilder $document
* @throws \OutOfBoundsException
*/
public function __construct(NodeInterface $ast, DocumentBuilder $document)
{
$this->boot($ast, $document);
}
/**
* @param NodeInterface $ast
* @return bool
* @throws TypeConflictException
* @throws \OutOfBoundsException
*/
protected function onCompile(NodeInterface $ast): bool
{
$type = DocumentBuilder::AST_TYPE_MAPPING[$ast->getName()] ?? null;
if ($type !== null && ! ($type instanceof ExtendDefinition)) {
/** @var Compilable $virtualType */
$virtualType = new $type($ast, $this->getDocument());
$virtualType->compile();
$this->applyExtender($virtualType);
}
return false;
}
/**
* @param Definition|Compilable $instance
* @return void
* @throws TypeConflictException
* @throws \OutOfBoundsException
*/
private function applyExtender(Definition $instance): void
{
$this->type = $this->load($instance->getName());
$this->extend($this->type, $instance);
}
/**
* @param Definition $original
* @param Definition $extend
* @return void
* @throws TypeConflictException
* @throws \OutOfBoundsException
*/
private function extend(Definition $original, Definition $extend): void
{
if ($original instanceof HasFields && $extend instanceof HasFields) {
$this->extendFields($original, $extend);
}
if ($original instanceof HasDirectives && $extend instanceof HasDirectives) {
$this->extendDirectives($original, $extend);
}
if ($original instanceof HasArguments && $extend instanceof HasArguments) {
$this->extendArguments($original, $extend);
}
}
/**
* @param HasFields $original
* @param HasFields $extend
* @return void
* @throws \OutOfBoundsException
* @throws TypeConflictException
*/
private function extendFields(HasFields $original, HasFields $extend): void
{
foreach ($extend->getFields() as $extendField) {
if ($original->hasField($extendField->getName())) {
/**
* Check field type.
* @var FieldDefinition $field
*/
$field = $original->getField($extendField->getName());
$this->getValidator(Inheritance::class)->validate($extendField, $field);
$this->dataFieldExtender()->call($field, $extendField);
/**
* Check field arguments
*/
$this->extendArguments($field, $extendField);
continue;
}
$callee = function () use ($extendField): void {
/** @var BaseFieldsContainer $this */
$this->fields[$extendField->getName()] = $extendField;
};
$callee->call($original);
}
}
/**
* @return \Closure
*/
private function dataFieldExtender(): \Closure
{
/** @var FieldDefinition|BaseField $field */
return function (FieldDefinition $field): void {
/** @var BaseField $this */
// Extend type
$this->type = $field->type;
// Extend deprecation reason
$this->deprecationReason = $field->deprecationReason ?: $this->deprecationReason;
// Extend description
$this->description = $field->description ?: $this->description;
// NonNull overriding
$this->isNonNull = $field->isNonNull();
$this->isListOfNonNulls = $field->isListOfNonNulls();
};
}
/**
* @param HasArguments|BaseArgumentsContainer|DirectiveInvocation $original
* @param HasArguments|DirectiveInvocation $extend
* @return void
* @throws \OutOfBoundsException
* @throws \Railt\SDL\Exceptions\TypeConflictException
*/
private function extendArguments($original, $extend): void
{
foreach ($extend->getArguments() as $extendArgument) {
if ($original->hasArgument($extendArgument->getName())) {
/**
* Check field type.
* @var ArgumentDefinition $argument
*/
$argument = $original->getArgument($extendArgument->getName());
$this->getValidator(Inheritance::class)->validate($extendArgument, $argument);
$this->dataArgumentExtender()->call($argument, $extendArgument);
continue;
}
$callee = function () use ($extendArgument): void {
/** @var BaseArgumentsContainer $this */
$this->arguments[$extendArgument->getName()] = $extendArgument;
};
$callee->call($original);
}
}
/**
* @return \Closure
*/
private function dataArgumentExtender(): \Closure
{
/** @var ArgumentDefinition|BaseArgument $argument */
return function (ArgumentDefinition $argument): void {
/** @var BaseArgument $this */
// Extend type
$this->type = $argument->type;
// Extend deprecation reason
$this->deprecationReason = $argument->deprecationReason ?: $this->deprecationReason;
// Extend description
$this->description = $argument->description ?: $this->description;
// NonNull overriding
$this->isNonNull = $argument->isNonNull();
$this->isListOfNonNulls = $argument->isListOfNonNulls();
};
}
/**
* @param HasDirectives|BaseDirectivesContainer $original
* @param HasDirectives $extend
* @return void
* @throws \OutOfBoundsException
* @throws TypeConflictException
*/
private function extendDirectives(HasDirectives $original, HasDirectives $extend): void
{
foreach ($extend->getDirectives() as $extendDirective) {
if ($original->hasDirective($extendDirective->getName())) {
/** @var DirectiveInvocation $directive */
$directive = $original->getDirective($extendDirective->getName());
$this->extendArguments($directive, $extendDirective);
continue;
}
$callee = function () use ($extendDirective): void {
/** @var BaseArgumentsContainer $this */
$this->arguments[$extendDirective->getName()] = $extendDirective;
};
$callee->call($original);
}
}
}