3413ee6f创建于 2025年12月22日历史提交
<?php

/*
 *
 *  ____            _        _   __  __ _                  __  __ ____
 * |  _ \ ___   ___| | _____| |_|  \/  (_)_ __   ___      |  \/  |  _ \
 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
 * |  __/ (_) | (__|   <  __/ |_| |  | | | | | |  __/_____| |  | |  __/
 * |_|   \___/ \___|_|\_\___|\__|_|  |_|_|_| |_|\___|     |_|  |_|_|
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * @author PocketMine Team
 * @link http://www.pocketmine.net/
 *
 *
 */

declare(strict_types=1);

namespace pocketmine\utils;

use function array_diff;
use function array_unshift;
use function array_values;

/**
 * Extend this class to define source values for a generated registry class.
 * A registry class has generated functions that simulate object constants.
 *
 * The values are defined in the {@link self::setup()} function.
 * The generated class's name is defined in {@link self::getTargetClassName()}. The generated class will belong to the
 * same namespace as your source class.
 *
 * The generated class will call your source class at runtime, so both the source and generated class must be included
 * in the build.
 *
 * To create a registry, simply extend this class and implement the abstract methods. Then, run the
 * registry-interface.php script in the build folder to generate the accessor class.
 *
 * This supersedes the older {@link RegistryTrait}, which is slower and less robust than this newer approach, and also
 * required patching source classes. This approach allows the generated code to be fully separate from the source code.
 *
 * @see build/codegen/registry-interface.php
 *
 * @phpstan-template TMember of object
 */
abstract class RegistrySource{

	/**
	 * @var mixed[]
	 * @phpstan-var array<string, TMember>
	 */
	private array $simpleMembers = [];

	/**
	 * @var \Closure[]
	 * @phpstan-var array<string, \Closure(string $name) : TMember>
	 */
	private array $delayedMembers = [];

	private static ?string $inSetupClass = null;

	final public function __construct(){
		//NOOP
	}

	/**
	 * Returns the short name (without namespace) of the class to be generated.
	 * The generated class will have the same namespace as your source class.
	 */
	abstract public function getTargetClassName() : string;

	/**
	 * Returns information to be prepended to the doc comment on the generated class.
	 * Do not include any PHPDoc formatting (e.g. comment tags) in here.
	 *
	 * @return string[]
	 * @phpstan-return list<string>
	 */
	public function getTargetClassDocComment() : array{
		return [];
	}

	/**
	 * Override this to return true if the registry members are mutable.
	 * Supersedes CloningRegistryTrait
	 */
	public function cloneResults() : bool{ return false; }

	/**
	 * Ensures that no other registry gets setup while this one is being set up, to prevent suspicious dependencies
	 */
	private function setupWrapper() : void{
		if(self::$inSetupClass !== null){
			throw new \LogicException("Registry source class " . self::$inSetupClass . " tried to reference a member of registry " . $this->getTargetClassName() . " without using registerDelayed()");
		}
		self::$inSetupClass = static::class;
		try{
			$this->setup();
		}finally{
			self::$inSetupClass = null;
		}
	}

	/**
	 * Implement this to add members to the registry.
	 *
	 * @see self::registerValue() for simple values which do not depend on any other registry members
	 * @see self::registerDelayed() for values which depend on other registry members, either in this registry or another
	 */
	abstract protected function setup() : void;

	/**
	 * Override this if you need to, for example, clone a registry member before returning it to the caller
	 *
	 * @phpstan-template TParam of object
	 *
	 * @phpstan-param TParam $member
	 * @phpstan-return TParam
	 */
	public static function preprocessMember(object $member) : object{
		return $member;
	}

	/**
	 * Adds a plain value to the registry source. You can use this if the value does not depend on any other generated
	 * registry code.
	 * The type of the generated registry function will be inferred from the value provided.
	 *
	 * @phpstan-param TMember $value
	 * @phpstan-return TMember
	 */
	final protected function registerValue(string $name, mixed $value) : mixed{
		if(isset($this->simpleMembers[$name]) || isset($this->delayedMembers[$name])){
			throw new \InvalidArgumentException("Cannot redeclare registry member \"$name\"");
		}
		$this->simpleMembers[$name] = $value;

		return $value;
	}

	/**
	 * Adds a value using a callback, which will be invoked when the registry is first accessed at runtime.
	 * The type of the generated registry function will be the same as that of the provided closure.
	 *
	 * Use this if the value's initialization depends on generated registry code (i.e. it accesses another registry
	 * member, either in this registry or another).
	 *
	 * Note: A return type MUST be set on the provided closure, or an error will be thrown.
	 *
	 * @phpstan-param \Closure(string $name) : TMember $valueFactory
	 */
	final protected function registerDelayed(string $name, \Closure $valueFactory) : void{
		if(isset($this->simpleMembers[$name]) || isset($this->delayedMembers[$name])){
			throw new \InvalidArgumentException("Cannot redeclare registry member \"$name\"");
		}
		Utils::validateCallableSignature(fn(string $name) : object => die(), $valueFactory);
		$this->delayedMembers[$name] = $valueFactory;
	}

	/**
	 * @internal Initializes and yields all registry values. By-value first, then delayed, in the order of registration.
	 *
	 * @return \Generator|object[]
	 * @phpstan-return \Generator<string, TMember, void, void>
	 */
	final public function getAllValues() : \Generator{
		$this->setupWrapper();
		yield from $this->simpleMembers;
		foreach(Utils::stringifyKeys($this->delayedMembers) as $name => $callback){
			yield $name => $callback($name);
		}
	}

	/**
	 * @internal Returns type info for all registry members for code generation, without initializing delayed members.
	 *
	 * @return string[][]
	 * @phpstan-return array<string, list<string>>
	 */
	final public function getAllDeclarations() : array{
		$this->setupWrapper();
		$memberTypes = [];
		foreach(Utils::stringifyKeys($this->simpleMembers) as $name => $value){
			$reflect = new \ReflectionClass($value);
			$concrete = $reflect;
			if($reflect->isAnonymous()){
				while($concrete !== false && $concrete->isAnonymous()){
					$concrete = $concrete->getParentClass();
				}

				if($concrete === false){
					$memberTypes[$name] = [];
				}else{
					$anonInterfaces = array_diff($reflect->getInterfaceNames(), $concrete->getInterfaceNames());
					array_unshift($anonInterfaces, $concrete->getName());
					$memberTypes[$name] = array_values($anonInterfaces);
				}
			}else{
				$memberTypes[$name] = [$reflect->getName()];
			}
		}

		foreach(Utils::stringifyKeys($this->delayedMembers) as $name => $callback){
			$return = (new \ReflectionFunction($callback))->getReturnType();
			if($return === null){
				\GlobalLogger::get()->warning("Delayed registry member " . $this->getTargetClassName() . "::" . $name . " doesn't have a return type, using \"object\"");
				$memberTypes[$name] = [];
			}elseif($return instanceof \ReflectionNamedType){
				$memberTypes[$name] = [$return->getName()];
			}elseif($return instanceof \ReflectionIntersectionType){
				$memberTypes[$name] = [];
				foreach($return->getTypes() as $type){
					if(!$type instanceof \ReflectionNamedType){
						throw new \InvalidArgumentException("Unsupported nested type in intersection type for \"$name\"");
					}
					$memberTypes[$name][] = $type->getName();
				}
			}else{
				throw new \LogicException("Unsupported delayed member type for \"$name\"");
			}
		}

		return $memberTypes;
	}
}