Skip to content

Consider expanding on getClassDefinitions to support dynamic instance setting #2

@alexstandiford

Description

@alexstandiford

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
        ],
    ];
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions