-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Currently getClassDefinitions allows you to register classes in one of two ways:
as a single concrete => abstract binding, or as a concrete => [array, of, abstracts] set of bindings
/** @inheritDoc */
public function getClassDefinitions(): array
{
return [
WordPressPluginConfigProvider::class => [
HasTextDomain::class,
HasRestNamespace::class,
HasLocalDatabasePrefix::class,
HasCacheKeyPrefix::class,
HasDefaultTtl::class,
PlatformContextProvider::class,
CanResolvePaths::class,
CanResolveUrls::class
],
JwtStrategy::class => JwtStrategyInterface::class
];
}
But it would be nice to be able to conditionally set which concrete is used for different interfaces. This could reduce our reliance on registries, and provider classes by allowing us to embed logic in a standardized way.
A plausible way to do this is with some kind of resolver:
interface ClassResolverInterface
{
public function resolve(RequestContext $context): ?string;
}
class RequestContext
{
protected string $requestedClass;
protected array $constructorDependencies;
protected array $implementedInterfaces;
public function __construct(string $requestedClass, array $constructorDependencies, array $implementedInterfaces)
{
$this->requestedClass = $requestedClass;
$this->constructorDependencies = $constructorDependencies;
$this->implementedInterfaces = $implementedInterfaces;
}
public function getRequestedClass(): string
{
return $this->requestedClass;
}
public function getConstructorDependencies(): array
{
return $this->constructorDependencies;
}
public function getImplementedInterfaces(): array
{
return $this->implementedInterfaces;
}
}
and then use that in getClassDefinitions
public function getClassDefinitions(): array
{
return [
WordPressPluginConfigProvider::class => [
HasTextDomain::class,
HasRestNamespace::class,
HasLocalDatabasePrefix::class,
HasCacheKeyPrefix::class,
HasDefaultTtl::class,
PlatformContextProvider::class,
CanResolvePaths::class,
CanResolveUrls::class,
],
JwtStrategy::class => JwtStrategyInterface::class
BasicCache::class => [
'bindings' => [CacheStrategyInterface::class],
'resolver' => CacheStrategyResolver::class, // Resolver for conditional binding
],
];
}
The cache in this example could conditionally use redis, but fallback to the basic cache if none is provided:
class CacheStrategyResolver implements ClassResolverInterface
{
public function resolve(RequestContext $context): ?string
{
// Check if RedisCache is better suited based on context information
if ($this->requiresRedisCache($context)) {
return RedisCache::class;
}
// Fall back to the default if conditions aren't met
return null;
}
protected function requiresRedisCache(RequestContext $context): bool
{
// Criterion 1: If the requested class specifically implements a certain interface
if (in_array(DistributedCacheInterface::class, $context->getImplementedInterfaces(), true)) {
return true;
}
// Criterion 2: If specific dependencies are needed, e.g., Redis client
$dependencies = $context->getConstructorDependencies();
if (in_array(RedisClient::class, $dependencies, true)) {
return true;
}
// No criteria met, default to BasicCache
return false;
}
}
class RequestContext
{
protected string $requestedClass;
protected array $constructorDependencies;
protected array $implementedInterfaces;
public function __construct(string $requestedClass, array $constructorDependencies, array $implementedInterfaces)
{
$this->requestedClass = $requestedClass;
$this->constructorDependencies = $constructorDependencies;
$this->implementedInterfaces = $implementedInterfaces;
}
public function getRequestedClass(): string
{
return $this->requestedClass;
}
public function getConstructorDependencies(): array
{
return $this->constructorDependencies;
}
public function getImplementedInterfaces(): array
{
return $this->implementedInterfaces;
}
}
I think this would also allow us to embed a registry pattern directly into the container logic, which could drastically simply our registries. Check out this example of a path resolver solution that allows us to register multiple paths automatically based on the namespace:
First we define a namespace registry that allows us to register multiple paths across the platform.
namespace PHPNomad\Template;
use PHPNomad\Template\Interfaces\CanResolvePaths;
class NamespaceRegistry
{
protected array $registry = [];
public function register(string $namespace, CanResolvePaths $resolver): void
{
$this->registry[$namespace] = $resolver;
}
public function getResolverForNamespace(string $namespace): ?CanResolvePaths
{
foreach ($this->registry as $registeredNamespace => $resolver) {
if (strpos($namespace, $registeredNamespace) === 0) {
return $resolver;
}
}
return null;
}
}
Then we set up the resolver:
namespace PHPNomad\Template\Resolvers;
use PHPNomad\Template\NamespaceRegistry;
use PHPNomad\Template\Interfaces\CanResolvePaths;
use PHPNomad\Core\RequestContext;
use PHPNomad\Core\ClassResolverInterface;
class PathStrategyResolver implements ClassResolverInterface
{
protected NamespaceRegistry $registry;
public function __construct(NamespaceRegistry $registry)
{
$this->registry = $registry;
}
public function resolve(RequestContext $context): ?string
{
// Get the requested class namespace
$namespace = $this->getNamespace($context->getRequestedClass());
// Retrieve a resolver based on the namespace
$resolver = $this->registry->getResolverForNamespace($namespace);
// Return the class name of the resolver if found; otherwise, null for default. Could optionally throw a DI exception here, too.
return $resolver ? get_class($resolver) : null;
}
protected function getNamespace(string $class): string
{
return substr($class, 0, strrpos($class, '\\'));
}
}
public function getClassDefinitions(): array
{
return [
DefaultPathResolver::class => [
'bindings' => CanResolvePaths::class,
'resolver' => PathStrategyResolver::class, // Uses PathStrategyResolver to resolve based on namespace
],
];
}