- Introduce `InitializerDependencyAnalyzer` to support dependency analysis during cyclic exceptions. - Add proactive initializer discovery with `InitializerCacheUpdater` for improved performance. - Integrate discovery cache updates and error handling for seamless caching of found initializers. - Extend `CyclicDependencyException` with `InitializerDependencyAnalyzer` for enhanced diagnostics and cycle analysis.
612 lines
24 KiB
PHP
612 lines
24 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\DI;
|
|
|
|
use App\Framework\Core\ValueObjects\ClassName;
|
|
use App\Framework\DI\Exceptions\ClassNotInstantiable;
|
|
use App\Framework\DI\Exceptions\ClassNotResolvableException;
|
|
use App\Framework\DI\Exceptions\ClassResolutionException;
|
|
use App\Framework\DI\ProactiveInitializerFinder;
|
|
use App\Framework\DI\ValueObjects\InitializerInfo;
|
|
use App\Framework\DI\Exceptions\ContainerException;
|
|
use App\Framework\DI\Exceptions\CyclicDependencyException;
|
|
use App\Framework\DI\Exceptions\LazyLoadingException;
|
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
|
use App\Framework\DI\FailedInitializerRegistry;
|
|
use App\Framework\DI\ValueObjects\FailedInitializer;
|
|
use App\Framework\Logging\Logger;
|
|
use App\Framework\Logging\ValueObjects\LogContext;
|
|
use App\Framework\Metrics\FrameworkMetricsCollector;
|
|
use App\Framework\Reflection\CachedReflectionProvider;
|
|
use App\Framework\Reflection\ReflectionProvider;
|
|
use Stringable;
|
|
use Throwable;
|
|
|
|
final class DefaultContainer implements Container
|
|
{
|
|
/** @var class-string[] */
|
|
private array $resolving = [];
|
|
|
|
private readonly DependencyResolver $dependencyResolver;
|
|
|
|
private readonly SingletonDetector $singletonDetector;
|
|
|
|
private readonly LazyInstantiator $lazyInstantiator;
|
|
|
|
public readonly MethodInvoker $invoker;
|
|
|
|
public readonly ContainerIntrospector $introspector;
|
|
|
|
public readonly FrameworkMetricsCollector $metrics;
|
|
|
|
private readonly InitializerDependencyAnalyzer $dependencyAnalyzer;
|
|
|
|
public function __construct(
|
|
private readonly InstanceRegistry $instances = new InstanceRegistry(),
|
|
private readonly BindingRegistry $bindings = new BindingRegistry(),
|
|
private readonly ReflectionProvider $reflectionProvider = new CachedReflectionProvider(),
|
|
) {
|
|
$this->dependencyResolver = new DependencyResolver($this->reflectionProvider, $this);
|
|
$this->singletonDetector = new SingletonDetector($this->reflectionProvider, $this->instances);
|
|
$this->lazyInstantiator = new LazyInstantiator(
|
|
$this->reflectionProvider,
|
|
$this->createInstance(...)
|
|
);
|
|
$this->invoker = new MethodInvoker($this, $this->reflectionProvider);
|
|
$this->introspector = new ContainerIntrospector(
|
|
$this,
|
|
$this->instances,
|
|
$this->bindings,
|
|
$this->reflectionProvider,
|
|
fn (): array => $this->resolving
|
|
);
|
|
$this->metrics = new FrameworkMetricsCollector();
|
|
$this->dependencyAnalyzer = new InitializerDependencyAnalyzer($this);
|
|
|
|
$this->registerSelf();
|
|
$this->instance(ReflectionProvider::class, $this->reflectionProvider);
|
|
}
|
|
|
|
public function bind(ClassName|Stringable|string $abstract, callable|string|object $concrete): void
|
|
{
|
|
$abstract = (string) $abstract;
|
|
$this->bindings->bind($abstract, $concrete);
|
|
|
|
// Only clear caches for valid class names, skip string keys like 'filesystem.storage.local'
|
|
if (class_exists($abstract) || interface_exists($abstract)) {
|
|
$this->clearCaches(ClassName::create($abstract));
|
|
}
|
|
}
|
|
|
|
public function singleton(ClassName|Stringable|string $abstract, callable|string|object $concrete): void
|
|
{
|
|
$abstract = (string) $abstract;
|
|
$this->bind($abstract, $concrete);
|
|
$this->instances->markAsSingleton($abstract);
|
|
}
|
|
|
|
public function instance(ClassName|Stringable|string $abstract, object $instance): void
|
|
{
|
|
$this->instances->setInstance($abstract, $instance);
|
|
}
|
|
|
|
/**
|
|
* @template T of object
|
|
* @param class-string<T>|ClassName|Stringable $class
|
|
* @return T
|
|
*/
|
|
public function get(ClassName|Stringable|string $class): object
|
|
{
|
|
$className = (string) $class;
|
|
|
|
// Bereits instanziierte Objekte zurückgeben
|
|
$singleton = $this->instances->getSingleton($className);
|
|
if ($singleton !== null) {
|
|
return $singleton;
|
|
}
|
|
|
|
if ($this->instances->hasInstance($className)) {
|
|
return $this->instances->getInstance($className);
|
|
}
|
|
|
|
// Lazy Loading versuchen
|
|
if ($this->lazyInstantiator->canUseLazyLoading($className, $this->instances)) {
|
|
try {
|
|
return $this->lazyInstantiator->createLazyInstance($className);
|
|
} catch (LazyLoadingException $e) {
|
|
// Log LazyLoading-Fehler und versuche Fallback (Logger ist immer verfügbar)
|
|
$this->get(Logger::class)->warning(
|
|
"Lazy loading failed for {$className}, falling back to normal instantiation",
|
|
LogContext::withException($e)
|
|
);
|
|
|
|
// Fallback: Versuche normale Instanziierung
|
|
try {
|
|
return $this->createInstance($className);
|
|
} catch (\Throwable $fallbackException) {
|
|
// Wenn Fallback auch fehlschlägt, ursprüngliche LazyLoadingException werfen
|
|
throw $e;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// Andere Exceptions direkt weiterwerfen
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
return $this->createInstance($className);
|
|
}
|
|
|
|
/**
|
|
* @template T of object
|
|
* @param class-string<T> $class
|
|
* @return T
|
|
*/
|
|
private function createInstance(string $class): object
|
|
{
|
|
if (in_array($class, $this->resolving, true)) {
|
|
throw new CyclicDependencyException(
|
|
dependencyChain: $this->resolving,
|
|
class: $class,
|
|
code: 0,
|
|
previous: null,
|
|
dependencyAnalyzer: $this->dependencyAnalyzer
|
|
);
|
|
}
|
|
|
|
$this->resolving[] = $class;
|
|
|
|
try {
|
|
$instance = $this->buildInstance($class);
|
|
|
|
// Only check singleton attribute for actual classes
|
|
if ((class_exists($class) || interface_exists($class)) &&
|
|
$this->singletonDetector->isSingleton(ClassName::create($class))) {
|
|
$this->instances->setSingleton($class, $instance);
|
|
} else {
|
|
$this->instances->setInstance($class, $instance);
|
|
}
|
|
|
|
return $instance;
|
|
} catch (Throwable $e) {
|
|
// Track resolution failures
|
|
$this->metrics->increment('container.resolve.failures');
|
|
|
|
throw $e;
|
|
} finally {
|
|
array_pop($this->resolving);
|
|
}
|
|
}
|
|
|
|
private function buildInstance(string $class): object
|
|
{
|
|
if ($this->bindings->hasBinding($class)) {
|
|
return $this->resolveBinding($class, $this->bindings->getBinding($class));
|
|
}
|
|
|
|
// For string keys without bindings, throw immediately
|
|
if (!class_exists($class) && !interface_exists($class)) {
|
|
throw new ClassNotResolvableException(
|
|
class: $class,
|
|
availableBindings: array_keys($this->bindings->getAllBindings())
|
|
);
|
|
}
|
|
|
|
$className = ClassName::create($class);
|
|
|
|
// Enhanced diagnostics for missing bindings
|
|
try {
|
|
$reflection = $this->reflectionProvider->getClass($className);
|
|
|
|
// Check if class is instantiable using the framework's method
|
|
if (! $reflection->isInstantiable()) {
|
|
// Proaktive Suche nach Initializern für Interfaces
|
|
if ($reflection->isInterface() && $this->tryFindAndRegisterInitializer($class)) {
|
|
// Initializer gefunden und registriert - versuche erneut zu resolven
|
|
return $this->get($class);
|
|
}
|
|
|
|
$this->throwDetailedBindingException($class);
|
|
}
|
|
|
|
$dependencies = $this->dependencyResolver->resolveDependencies($className);
|
|
|
|
return $reflection->newInstance(...$dependencies->toArray());
|
|
} catch (ContainerException $e) {
|
|
// If it's already a ContainerException, just re-throw
|
|
throw $e;
|
|
} catch (\RuntimeException|\ReflectionException $e) {
|
|
// Wrap with binding information
|
|
throw ClassResolutionException::fromRuntimeException(
|
|
class: $class,
|
|
exception: $e,
|
|
availableBindings: array_keys($this->bindings->getAllBindings()),
|
|
dependencyChain: $this->resolving
|
|
);
|
|
}
|
|
}
|
|
|
|
private function throwDetailedBindingException(string $class): never
|
|
{
|
|
$availableBindings = array_keys($this->bindings->getAllBindings());
|
|
|
|
// Bestimme den Typ der Klasse mit Reflection
|
|
$className = ClassName::create($class);
|
|
$isInterface = false;
|
|
$isAbstract = false;
|
|
$isTrait = false;
|
|
|
|
try {
|
|
$reflection = $this->reflectionProvider->getClass($className);
|
|
$isInterface = $reflection->isInterface();
|
|
$isAbstract = $reflection->isAbstract();
|
|
$isTrait = $reflection->isTrait();
|
|
} catch (\Throwable $e) {
|
|
// Fallback zu einfachen Prüfungen wenn Reflection fehlschlägt
|
|
$isInterface = interface_exists($class);
|
|
// Für Abstract und Trait können wir nicht so einfach prüfen, aber wir haben bereits
|
|
// festgestellt dass die Klasse nicht instanziierbar ist
|
|
}
|
|
|
|
// Try to get DiscoveryRegistry from container and include discovered initializers
|
|
$discoveredInitializers = [];
|
|
$matchingInitializers = [];
|
|
$suggestedInitializer = null;
|
|
$failedInitializer = null;
|
|
|
|
if ($this->has(DiscoveryRegistry::class)) {
|
|
try {
|
|
$discoveryRegistry = $this->get(DiscoveryRegistry::class);
|
|
$initializerResults = $discoveryRegistry->attributes->get(Initializer::class);
|
|
|
|
if (! empty($initializerResults)) {
|
|
$discoveredInitializers = array_map(
|
|
fn($attr) => $attr->className->getFullyQualified(),
|
|
$initializerResults
|
|
);
|
|
|
|
// Spezielle Behandlung für Interfaces: Suche nach Initializern die dieses Interface zurückgeben
|
|
if ($isInterface) {
|
|
foreach ($initializerResults as $initializer) {
|
|
$returnType = $initializer->additionalData['return'] ?? null;
|
|
if ($returnType === $class) {
|
|
$matchingInitializers[] = $initializer->className->getFullyQualified();
|
|
}
|
|
}
|
|
|
|
// Vorschlag basierend auf Interface-Name (z.B. ComponentRegistryInterface -> ComponentRegistryInitializer)
|
|
$interfaceName = basename(str_replace('\\', '/', $class));
|
|
$suggestedName = str_replace('Interface', '', $interfaceName) . 'Initializer';
|
|
|
|
foreach ($discoveredInitializers as $initializer) {
|
|
if (str_contains($initializer, $suggestedName)) {
|
|
$suggestedInitializer = $initializer;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// Silently ignore errors when trying to access DiscoveryRegistry to avoid masking the original error
|
|
}
|
|
}
|
|
|
|
// Prüfe ob ein Initializer für dieses Interface fehlgeschlagen ist
|
|
if ($this->has(FailedInitializerRegistry::class)) {
|
|
try {
|
|
$failedRegistry = $this->get(FailedInitializerRegistry::class);
|
|
if ($failedRegistry->hasFailedInitializer($class)) {
|
|
$failedInitializer = $failedRegistry->getFailedInitializer($class);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// Silently ignore errors when trying to access FailedInitializerRegistry
|
|
}
|
|
}
|
|
|
|
throw ClassNotInstantiable::fromContainerContext(
|
|
class: $class,
|
|
dependencyChain: $this->resolving,
|
|
availableBindings: $availableBindings,
|
|
discoveredInitializers: $discoveredInitializers,
|
|
matchingInitializers: $matchingInitializers,
|
|
suggestedInitializer: $suggestedInitializer,
|
|
failedInitializer: $failedInitializer,
|
|
isInterface: $isInterface,
|
|
isAbstract: $isAbstract,
|
|
isTrait: $isTrait
|
|
);
|
|
}
|
|
|
|
private function resolveBinding(string $class, callable|string|object $concrete): object
|
|
{
|
|
try {
|
|
return match (true) {
|
|
is_callable($concrete) => $concrete($this),
|
|
is_string($concrete) => $this->get($concrete),
|
|
/* @var object $concrete */
|
|
default => $concrete
|
|
};
|
|
} catch (ContainerException $e) {
|
|
// Re-throw ContainerExceptions as-is (they already have proper context)
|
|
throw $e;
|
|
} catch (\Throwable $e) {
|
|
// Determine binding type for better error messages
|
|
$bindingType = match (true) {
|
|
is_callable($concrete) => 'callable',
|
|
is_string($concrete) => 'string',
|
|
default => 'object'
|
|
};
|
|
|
|
// Für callable bindings: Prüfe ob ein Initializer fehlgeschlagen ist
|
|
$failedInitializer = null;
|
|
$matchingInitializer = null;
|
|
|
|
if ($bindingType === 'callable') {
|
|
// Prüfe ob ein fehlgeschlagener Initializer für diese Klasse existiert
|
|
if ($this->has(FailedInitializerRegistry::class)) {
|
|
try {
|
|
$failedRegistry = $this->get(FailedInitializerRegistry::class);
|
|
if ($failedRegistry->hasFailedInitializer($class)) {
|
|
$failedInitializer = $failedRegistry->getFailedInitializer($class);
|
|
}
|
|
} catch (\Throwable $registryError) {
|
|
// Silently ignore errors when trying to access FailedInitializerRegistry
|
|
}
|
|
}
|
|
|
|
// Suche auch in DiscoveryRegistry nach Initializern die diese Klasse zurückgeben
|
|
if ($failedInitializer === null && $this->has(DiscoveryRegistry::class)) {
|
|
try {
|
|
$discoveryRegistry = $this->get(DiscoveryRegistry::class);
|
|
$initializerResults = $discoveryRegistry->attributes->get(Initializer::class);
|
|
|
|
if (! empty($initializerResults)) {
|
|
foreach ($initializerResults as $initializer) {
|
|
$returnType = $initializer->additionalData['return'] ?? null;
|
|
if ($returnType === $class) {
|
|
$matchingInitializer = $initializer->className->getFullyQualified();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} catch (\Throwable $registryError) {
|
|
// Silently ignore errors when trying to access DiscoveryRegistry
|
|
}
|
|
}
|
|
}
|
|
|
|
throw ClassResolutionException::fromBindingResolution(
|
|
class: $class,
|
|
previous: $e,
|
|
availableBindings: array_keys($this->bindings->getAllBindings()),
|
|
dependencyChain: $this->resolving,
|
|
bindingType: $bindingType,
|
|
failedInitializer: $failedInitializer,
|
|
matchingInitializer: $matchingInitializer
|
|
);
|
|
}
|
|
}
|
|
|
|
/** @param class-string $class */
|
|
public function has(ClassName|Stringable|string $class): bool
|
|
{
|
|
$class = (string) $class;
|
|
return $this->instances->hasSingleton($class)
|
|
|| $this->instances->hasInstance($class)
|
|
|| $this->bindings->hasBinding($class)
|
|
|| $this->canAutoWire($class);
|
|
}
|
|
|
|
/**
|
|
* Prüft ob eine Klasse automatisch instanziiert werden kann (auto-wiring)
|
|
* @param string $class Klassenname
|
|
*/
|
|
private function canAutoWire(string $class): bool
|
|
{
|
|
if (empty($class)) {
|
|
return false;
|
|
}
|
|
|
|
$className = ClassName::create($class);
|
|
if (! $className->exists()) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return $this->reflectionProvider->getClass($className)->isInstantiable();
|
|
} catch (\Throwable $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** @param class-string $class */
|
|
public function forget(ClassName|Stringable|string $class): void
|
|
{
|
|
$class = (string) $class;
|
|
$this->instances->forget($class);
|
|
$this->bindings->forget($class);
|
|
$this->clearCaches(ClassName::create($class));
|
|
}
|
|
|
|
public function flush(): void
|
|
{
|
|
$this->instances->flush();
|
|
$this->bindings->flush();
|
|
$this->reflectionProvider->flush();
|
|
$this->dependencyResolver->flushCache();
|
|
$this->lazyInstantiator->flushFactories();
|
|
|
|
// Container selbst wieder registrieren
|
|
$this->registerSelf();
|
|
}
|
|
|
|
public function getRegisteredServices(): array
|
|
{
|
|
return array_merge(
|
|
$this->instances->getAllRegistered(),
|
|
$this->bindings->getAllBindings()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get all registered service IDs (for debugging/admin)
|
|
* @return array<string>
|
|
*/
|
|
public function getServiceIds(): array
|
|
{
|
|
return array_keys($this->getRegisteredServices());
|
|
}
|
|
|
|
private function clearCaches(ClassName $className): void
|
|
{
|
|
$this->reflectionProvider->forget($className);
|
|
$this->dependencyResolver->clearCache($className);
|
|
$this->lazyInstantiator->forgetFactory($className->getFullyQualified());
|
|
}
|
|
|
|
private function registerSelf(): void
|
|
{
|
|
$this->instances->setSingleton(self::class, $this);
|
|
$this->instances->setSingleton(DefaultContainer::class, $this);
|
|
$this->instances->setSingleton(Container::class, $this);
|
|
}
|
|
|
|
/**
|
|
* Lazy-getter für ProactiveInitializerFinder
|
|
*/
|
|
private function getProactiveFinder(): ProactiveInitializerFinder
|
|
{
|
|
// Erstelle Finder nur wenn benötigt
|
|
$discoveryRegistry = $this->has(DiscoveryRegistry::class) ? $this->get(DiscoveryRegistry::class) : null;
|
|
if ($discoveryRegistry === null) {
|
|
// Fallback: Leere Registry erstellen
|
|
$discoveryRegistry = \App\Framework\Discovery\Results\DiscoveryRegistry::empty();
|
|
}
|
|
|
|
$fileScanner = $this->has(\App\Framework\Filesystem\FileScanner::class)
|
|
? $this->get(\App\Framework\Filesystem\FileScanner::class)
|
|
: new \App\Framework\Filesystem\FileScanner(null, null, new \App\Framework\Filesystem\FileSystemService());
|
|
|
|
$fileSystemService = new \App\Framework\Filesystem\FileSystemService();
|
|
$classExtractor = new \App\Framework\Discovery\Processing\ClassExtractor($fileSystemService);
|
|
|
|
$pathProvider = $this->has(\App\Framework\Core\PathProvider::class)
|
|
? $this->get(\App\Framework\Core\PathProvider::class)
|
|
: new \App\Framework\Core\PathProvider(getcwd() ?: '.');
|
|
|
|
return new ProactiveInitializerFinder(
|
|
reflectionProvider: $this->reflectionProvider,
|
|
discoveryRegistry: $discoveryRegistry,
|
|
fileScanner: $fileScanner,
|
|
classExtractor: $classExtractor,
|
|
pathProvider: $pathProvider
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Versucht proaktiv einen Initializer für ein Interface zu finden und zu registrieren
|
|
*
|
|
* @param string $interface Interface-Klasse
|
|
* @return bool True wenn Initializer gefunden und registriert wurde, false sonst
|
|
*/
|
|
private function tryFindAndRegisterInitializer(string $interface): bool
|
|
{
|
|
try {
|
|
$finder = $this->getProactiveFinder();
|
|
$initializerInfo = $finder->findInitializerForInterface($interface);
|
|
|
|
if ($initializerInfo === null) {
|
|
return false;
|
|
}
|
|
|
|
// Registriere Initializer als lazy binding
|
|
$initializerClass = $initializerInfo->initializerClass->getFullyQualified();
|
|
$methodName = $initializerInfo->methodName->toString();
|
|
|
|
$this->singleton($interface, function (Container $container) use ($initializerClass, $methodName) {
|
|
$instance = $container->get($initializerClass);
|
|
return $container->invoker->invoke($initializerClass, $methodName);
|
|
});
|
|
|
|
// Gefundenen Initializer in Discovery Cache nachtragen
|
|
$this->updateDiscoveryCache($initializerInfo);
|
|
|
|
return true;
|
|
} catch (\Throwable $e) {
|
|
// Silently ignore errors during proactive search
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Aktualisiert den Discovery Cache mit einem proaktiv gefundenen Initializer
|
|
*/
|
|
private function updateDiscoveryCache(InitializerInfo $initializerInfo): void
|
|
{
|
|
try {
|
|
$updater = $this->getCacheUpdater();
|
|
if ($updater === null) {
|
|
return;
|
|
}
|
|
|
|
// Versuche zuerst Registry aus Container zu laden
|
|
$registry = $this->has(DiscoveryRegistry::class)
|
|
? $this->get(DiscoveryRegistry::class)
|
|
: null;
|
|
|
|
$updater->updateCache($initializerInfo, $registry);
|
|
} catch (\Throwable $e) {
|
|
// Silently ignore errors during cache update
|
|
// Initializer funktioniert trotzdem, nur Cache-Update schlägt fehl
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Holt oder erstellt einen InitializerCacheUpdater
|
|
*/
|
|
private function getCacheUpdater(): ?InitializerCacheUpdater
|
|
{
|
|
try {
|
|
// Prüfe ob alle benötigten Abhängigkeiten verfügbar sind
|
|
if (!$this->has(\App\Framework\Discovery\Storage\DiscoveryCacheManager::class)) {
|
|
// Versuche DiscoveryCacheManager zu erstellen
|
|
if (!$this->has(\App\Framework\Cache\Cache::class) ||
|
|
!$this->has(\App\Framework\DateTime\Clock::class) ||
|
|
!$this->has(\App\Framework\Filesystem\FileSystemService::class)) {
|
|
return null;
|
|
}
|
|
|
|
$cache = $this->get(\App\Framework\Cache\Cache::class);
|
|
$clock = $this->get(\App\Framework\DateTime\Clock::class);
|
|
$fileSystemService = $this->get(\App\Framework\Filesystem\FileSystemService::class);
|
|
|
|
$cacheManager = new \App\Framework\Discovery\Storage\DiscoveryCacheManager(
|
|
cache: $cache,
|
|
clock: $clock,
|
|
fileSystemService: $fileSystemService
|
|
);
|
|
} else {
|
|
$cacheManager = $this->get(\App\Framework\Discovery\Storage\DiscoveryCacheManager::class);
|
|
}
|
|
|
|
$pathProvider = $this->has(\App\Framework\Core\PathProvider::class)
|
|
? $this->get(\App\Framework\Core\PathProvider::class)
|
|
: new \App\Framework\Core\PathProvider(getcwd() ?: '.');
|
|
|
|
$clock = $this->has(\App\Framework\DateTime\Clock::class)
|
|
? $this->get(\App\Framework\DateTime\Clock::class)
|
|
: new \App\Framework\DateTime\SystemClock();
|
|
|
|
return new InitializerCacheUpdater(
|
|
reflectionProvider: $this->reflectionProvider,
|
|
cacheManager: $cacheManager,
|
|
pathProvider: $pathProvider,
|
|
clock: $clock
|
|
);
|
|
} catch (\Throwable $e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|