PocketMine-MP/src/thread/ThreadSafeClassLoader.php
2023-12-20 17:01:20 +00:00

183 lines
5.1 KiB
PHP

<?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\thread;
use pmmp\thread\ThreadSafe;
use pmmp\thread\ThreadSafeArray;
use function class_exists;
use function count;
use function explode;
use function file_exists;
use function interface_exists;
use function method_exists;
use function spl_autoload_register;
use function str_replace;
use function strrpos;
use function substr;
use function trait_exists;
use function trim;
use const DIRECTORY_SEPARATOR;
/**
* This autoloader can be used and updated from multiple threads.
* Useful if classes need to be dynamically added after threads have already been started.
*
* This is used to facilitate loading plugin classes, enabling plugins to be loaded after the server has started.
*/
class ThreadSafeClassLoader extends ThreadSafe{
/**
* @var ThreadSafeArray|string[]
* @phpstan-var ThreadSafeArray<int, string>
*/
private ThreadSafeArray $fallbackLookup;
/**
* @var ThreadSafeArray|string[][]
* @phpstan-var ThreadSafeArray<string, ThreadSafeArray<int, string>>
*/
private ThreadSafeArray $psr4Lookup;
public function __construct(){
$this->fallbackLookup = new ThreadSafeArray();
$this->psr4Lookup = new ThreadSafeArray();
}
protected function normalizePath(string $path) : string{
$parts = explode("://", $path, 2);
if(count($parts) === 2){
return $parts[0] . "://" . str_replace('/', DIRECTORY_SEPARATOR, $parts[1]);
}
return str_replace('/', DIRECTORY_SEPARATOR, $parts[0]);
}
public function addPath(string $namespacePrefix, string $path, bool $prepend = false) : void{
$path = $this->normalizePath($path);
if($namespacePrefix === '' || $namespacePrefix === '\\'){
$this->fallbackLookup->synchronized(function() use ($path, $prepend) : void{
$this->appendOrPrependLookupEntry($this->fallbackLookup, $path, $prepend);
});
}else{
$namespacePrefix = trim($namespacePrefix, '\\') . '\\';
$this->psr4Lookup->synchronized(function() use ($namespacePrefix, $path, $prepend) : void{
$list = $this->psr4Lookup[$namespacePrefix] ?? null;
if($list === null){
$list = $this->psr4Lookup[$namespacePrefix] = new ThreadSafeArray();
}
$this->appendOrPrependLookupEntry($list, $path, $prepend);
});
}
}
/**
* @phpstan-param ThreadSafeArray<int, string> $list
*/
protected function appendOrPrependLookupEntry(ThreadSafeArray $list, string $entry, bool $prepend) : void{
if($prepend){
$entries = $this->getAndRemoveLookupEntries($list);
$list[] = $entry;
foreach($entries as $removedEntry){
$list[] = $removedEntry;
}
}else{
$list[] = $entry;
}
}
/**
* @return string[]
*
* @phpstan-param ThreadSafeArray<int, string> $list
* @phpstan-return list<string>
*/
protected function getAndRemoveLookupEntries(ThreadSafeArray $list) : array{
$entries = [];
while(($entry = $list->shift()) !== null){
$entries[] = $entry;
}
return $entries;
}
public function register(bool $prepend = false) : bool{
return spl_autoload_register(function(string $name) : void{
$this->loadClass($name);
}, true, $prepend);
}
/**
* Called when there is a class to load
*/
public function loadClass(string $name) : bool{
$path = $this->findClass($name);
if($path !== null){
include($path);
if(!class_exists($name, false) && !interface_exists($name, false) && !trait_exists($name, false)){
return false;
}
if(method_exists($name, "onClassLoaded") && (new \ReflectionClass($name))->getMethod("onClassLoaded")->isStatic()){
$name::onClassLoaded();
}
return true;
}
return false;
}
/**
* Returns the path for the class, if any
*/
public function findClass(string $name) : ?string{
$baseName = str_replace("\\", DIRECTORY_SEPARATOR, $name);
foreach($this->fallbackLookup as $path){
$filename = $path . DIRECTORY_SEPARATOR . $baseName . ".php";
if(file_exists($filename)){
return $filename;
}
}
// PSR-4 lookup
$logicalPathPsr4 = $baseName . ".php";
return $this->psr4Lookup->synchronized(function() use ($name, $logicalPathPsr4) : ?string{
$subPath = $name;
while(false !== $lastPos = strrpos($subPath, '\\')){
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
$lookup = $this->psr4Lookup[$search] ?? null;
if($lookup !== null){
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach($lookup as $dir){
if(file_exists($file = $dir . $pathEnd)){
return $file;
}
}
}
}
return null;
});
}
}