*/ private array $dumpedTypes = []; public function __construct( private ClassDefinitionRepository $classDefinitionRepository, private ObjectBuilderFactory $objectBuilderFactory, private InterfaceInferringContainer $interfaceInferringContainer, private FunctionsContainer $constructors, ) {} public function dump(Type $type): string { return $this->dumpedTypes[$type->toString()] ??= $this->dumpAndQuote($type); } private function dumpAndQuote(Type $type): string { $context = $this->doDump($type, new TypeDumpContext()); if ($type instanceof FixedType || $type instanceof EnumType || $type instanceof VacantType) { return $context->read(); } return '`' . $context->read() . '`'; } private function doDump(Type $type, TypeDumpContext $context): TypeDumpContext { return match (true) { $type instanceof DumpableType => $this->fromDumpableType($type, $context), $type instanceof EnumType => $context->write($type->readableSignature()), $type instanceof NativeClassType => $this->fromObjectType($type, $context), $type instanceof InterfaceType => $this->fromInterfaceType($type, $context), $type instanceof VacantType => $context->write('*unknown*'), default => $context->write($type->toString()), }; } private function fromDumpableType(DumpableType $type, TypeDumpContext $context): TypeDumpContext { foreach ($type->dumpParts() as $item) { if (is_string($item)) { $context = $context->write($item); } else { $context = $this->doDump($item, $context); } } return $context; } /** * Loops on all constructors that can be used to instantiate a class. For * each one, we format its arguments to a shaped array which represents the * available data structure needed to construct the object. */ private function fromObjectType(ObjectType $type, TypeDumpContext $context): TypeDumpContext { if ($context->typeIsRetained($type)) { return $context->write('array{…}'); } $context = $context->retain($type); $class = $this->classDefinitionRepository->for($type); $objectBuilders = $this->objectBuilderFactory->for($class); usort($objectBuilders, fn ($a, $b) => $this->argumentsWeight($a->describeArguments()) <=> $this->argumentsWeight($b->describeArguments())); $arguments = array_map( static fn (ObjectBuilder $builder) => $builder->describeArguments(), $objectBuilders, ); $context = $this->formatArguments($arguments, $context); $context = $context->forgetLastRetained(); return $context; } /** * Dumping an interface type works in three steps: * * 1. We check if a constructor has been registered for this interface, in * which case we delegate the dump to the object dumper. * 2. If the interface can be inferred by using the `MapperBuilder::infer()` * method, we fetch the required arguments. * 3. For each possible class implementation of this interface we fetch all * constructors of the class, we merge their arguments with the arguments * from step 2, then we transform them to a shaped array which represents * the available data structure needed to construct the object. */ private function fromInterfaceType(InterfaceType $type, TypeDumpContext $context): TypeDumpContext { if ($context->isTooLong()) { return $context->write('array{…}'); } if ($this->constructorRegisteredFor($type)) { return $this->fromObjectType($type, $context); } if (! $this->interfaceInferringContainer->has($type->className())) { return $context->write('*unknown*'); } $function = $this->interfaceInferringContainer->inferFunctionFor($type->className()); $interfaceArguments = Arguments::fromParameters($function->parameters); try { $classTypes = $this->interfaceInferringContainer->classImplementationsFor($type->className()); } catch (ObjectImplementationCallbackError) { return $context->write('*unknown*'); } $classArguments = []; foreach ($classTypes as $classType) { $class = $this->classDefinitionRepository->for($classType); $objectBuilders = $this->objectBuilderFactory->for($class); foreach ($objectBuilders as $builder) { $arguments = $builder->describeArguments(); // We use the arguments hash to prevent constructor duplicates // that can be shared between different classes. $classArguments[$this->argumentsHash($arguments)] = $interfaceArguments->merge($arguments); } } usort($classArguments, fn ($a, $b) => $this->argumentsWeight($a) <=> $this->argumentsWeight($b)); return $this->formatArguments($classArguments, $context); } /** * @param non-empty-array $argumentsList */ private function formatArguments(array $argumentsList, TypeDumpContext $context): TypeDumpContext { while ($arguments = array_shift($argumentsList)) { if (count($arguments) === 1) { $context = $this->doDump($arguments->at(0)->type(), $context); $context = $context->write('|'); } if ($context->isTooLong()) { return $context->write('array{…}'); } else { $arguments = $arguments->toArray(); $context = $context->write('array{'); while ($argument = array_shift($arguments)) { $context = $context->write(sprintf('%s%s: ', $argument->name(), $argument->isRequired() ? '' : '?')); $context = $this->doDump($argument->type(), $context); if ($arguments !== []) { $context = $context->write(', '); } } $context = $context->write('}'); } if ($argumentsList !== []) { $context = $context->write('|'); } } return $context; } private function argumentsWeight(Arguments $arguments): int { return array_reduce( array: $arguments->toArray(), callback: static fn ($weight, Argument $argument) => $weight + TypeHelper::typePriority($argument->type()), // @infection-ignore-all (the initial value does not really matter here) initial: 0, ); } private function argumentsHash(Arguments $arguments): string { $arguments = $arguments->toArray(); ksort($arguments); $arguments = array_map(static fn (Argument $argument) => $argument->type()->toString(), $arguments); return hash('xxh128', serialize($arguments)); } private function constructorRegisteredFor(Type $type): bool { foreach ($this->constructors as $constructor) { if ($type->matches($constructor->definition->returnType)) { return true; } } return false; } }