Skip to content

Sources

Sources are providers that can fetch values from different storage locations. Cascade includes several built-in source types to cover common use cases.

All sources implement SourceInterface:

interface SourceInterface
{
/** Get the source name */
public function getName(): string;
/** Check if this source supports the given key/context */
public function supports(string $key, array $context): bool;
/** Attempt to resolve a value (returns null if not found) */
public function get(string $key, array $context): mixed;
/** Get metadata about this source */
public function getMetadata(): array;
}

The most flexible source type - uses closures to fetch values.

use Cline\Cascade\Source\CallbackSource;
$source = new CallbackSource(
name: 'database',
resolver: function(string $key, array $context) {
return $this->db
->table('settings')
->where('key', $key)
->value('value');
}
);

Only query this source when certain conditions are met:

$customerSource = new CallbackSource(
name: 'customer-db',
resolver: fn($key, $ctx) => $this->customerDb->find($ctx['customer_id'], $key),
supports: fn($key, $ctx) => isset($ctx['customer_id']),
);
// Source is skipped when customer_id is not in context

Transform values after retrieval:

$encryptedSource = new CallbackSource(
name: 'encrypted-db',
resolver: fn($key) => $this->db->find($key),
transformer: fn($row) => $this->decrypt($row->encrypted_value),
);
$source = new CallbackSource(
name: 'customer-credentials',
resolver: function(string $carrier, array $context) {
return $this->credentials
->where('customer_id', $context['customer_id'])
->where('carrier', $carrier)
->first()?->credentials;
},
supports: function(string $carrier, array $context) {
return isset($context['customer_id'])
&& $this->customers->exists($context['customer_id']);
},
transformer: function(array $credentials) {
return [
'api_key' => $this->decrypt($credentials['api_key']),
'api_secret' => $this->decrypt($credentials['api_secret']),
];
},
);

Static in-memory values, perfect for defaults and testing.

use Cline\Cascade\Source\ArraySource;
$defaults = new ArraySource('defaults', [
'api-timeout' => 30,
'max-retries' => 3,
'debug' => false,
]);
$cascade = Cascade::from()->fallbackTo($defaults);
$timeout = $cascade->get('api-timeout'); // 30

ArraySource supports nested keys:

$config = new ArraySource('config', [
'database' => [
'host' => 'localhost',
'port' => 5432,
],
'cache' => [
'driver' => 'redis',
'ttl' => 3600,
],
]);
// Access with dot notation
$host = $cascade->get('database.host'); // 'localhost'
$driver = $cascade->get('cache.driver'); // 'redis'

Build arrays dynamically:

$testCredentials = new ArraySource('test-mode', [
'fedex' => $this->generateTestCredentials('fedex'),
'ups' => $this->generateTestCredentials('ups'),
'dhl' => $this->generateTestCredentials('dhl'),
]);

Decorator that adds PSR-16 caching to any source.

use Cline\Cascade\Source\CacheSource;
$dbSource = new CallbackSource(
name: 'database',
resolver: fn($key) => $this->db->find($key),
);
$cachedSource = new CacheSource(
name: 'cached-db',
inner: $dbSource,
cache: $this->cache, // PSR-16 CacheInterface
ttl: 300, // 5 minutes
);

Generate cache keys based on context:

$cachedSource = new CacheSource(
name: 'cached-customer-db',
inner: $customerSource,
cache: $this->cache,
ttl: 300,
keyGenerator: function(string $key, array $context): string {
return sprintf(
'cascade:%s:%s:%s',
$context['customer_id'],
$context['environment'] ?? 'default',
$key
);
},
);
$cachedSource = new CacheSource(
name: 'cached-api',
inner: $apiSource,
cache: $this->cache,
ttl: fn($key) => match($key) {
'api-key' => 3600, // 1 hour
'api-secret' => 86400, // 24 hours
default => 300, // 5 minutes
},
);
use Psr\SimpleCache\CacheInterface;
class CachedCredentialSource
{
public function __construct(
private CallbackSource $inner,
private CacheInterface $cache,
) {}
public function create(): CacheSource
{
return new CacheSource(
name: 'cached-credentials',
inner: $this->inner,
cache: $this->cache,
ttl: 600, // 10 minutes
keyGenerator: fn($carrier, $ctx) => sprintf(
'cascade:credentials:%s:%s',
$ctx['customer_id'] ?? 'platform',
$carrier
),
);
}
}

Nest a complete cascade as a source (cascade within cascade).

use Cline\Cascade\Source\ChainedSource;
// Inner cascade for tenant resolution
$tenantCascade = Cascade::from()
->fallbackTo($tenantSource, priority: 1)
->fallbackTo($planSource, priority: 2)
->fallbackTo($defaultSource, priority: 3);
// Use as a source in parent cascade
$chainedSource = new ChainedSource(
name: 'tenant-cascade',
cascade: $tenantCascade,
);
$parentCascade = Cascade::from()
->fallbackTo($customerSource, priority: 1)
->fallbackTo($chainedSource, priority: 2);

Build complex multi-level resolution:

// Level 1: Plan tier defaults
$planCascade = Cascade::from()
->fallbackTo($enterprisePlanSource)
->fallbackTo($businessPlanSource)
->fallbackTo($freePlanSource);
// Level 2: Organization settings with plan fallback
$orgCascade = Cascade::from()
->fallbackTo($orgSource, priority: 1)
->fallbackTo(new ChainedSource('plan', $planCascade), priority: 2);
// Level 3: User settings with org fallback
$userCascade = Cascade::from()
->fallbackTo($userSource, priority: 1)
->fallbackTo(new ChainedSource('org', $orgCascade), priority: 2);
// Resolution: user → org → plan (tier) → system defaults
$value = $userCascade->get('feature-limit', [
'user_id' => 'user-123',
'org_id' => 'org-456',
'plan_tier' => 'enterprise',
]);

Chain sources only when context supports it:

$premiumCascade = Cascade::from()
->fallbackTo($premiumFeatureSource)
->fallbackTo($enhancedLimitSource);
$conditionalChain = new ChainedSource(
name: 'premium-features',
cascade: $premiumCascade,
supports: fn($key, $ctx) => ($ctx['plan'] ?? null) === 'premium',
);
$cascade = Cascade::from()
->fallbackTo($standardSource, priority: 1)
->fallbackTo($conditionalChain, priority: 2);
// Premium context uses chained source
$value = $cascade->get('rate-limit', ['plan' => 'premium']);

Always returns null - useful for testing and placeholders.

use Cline\Cascade\Source\NullSource;
// Test that fallback works when primary source has no value
$cascade = Cascade::from()
->fallbackTo(new NullSource('empty-primary'))
->fallbackTo(new ArraySource('fallback', ['key' => 'value']));
expect($cascade->get('key'))->toBe('value');

Create sources that will be implemented later:

$cascade = Cascade::from()
->fallbackTo(new NullSource('future-api-source'))
->fallbackTo($workingSource);
// Application works with fallback until API source is implemented

Implement SourceInterface for custom behavior:

use Cline\Cascade\Source\SourceInterface;
class RedisSource implements SourceInterface
{
public function __construct(
private string $name,
private Redis $redis,
private string $prefix = 'config:',
) {}
public function getName(): string
{
return $this->name;
}
public function supports(string $key, array $context): bool
{
return true; // Always try Redis
}
public function get(string $key, array $context): mixed
{
$redisKey = $this->prefix . $key;
$value = $this->redis->get($redisKey);
return $value !== false ? json_decode($value, true) : null;
}
public function getMetadata(): array
{
return [
'type' => 'redis',
'prefix' => $this->prefix,
];
}
}
// Usage
$redisSource = new RedisSource('redis-config', $redis, 'app:config:');
$cascade = Cascade::from()->fallbackTo($redisSource);
class EnvSource implements SourceInterface
{
public function __construct(
private string $name,
private array $mapping = [],
) {}
public function getName(): string
{
return $this->name;
}
public function supports(string $key, array $context): bool
{
$envKey = $this->mapping[$key] ?? strtoupper($key);
return isset($_ENV[$envKey]);
}
public function get(string $key, array $context): mixed
{
$envKey = $this->mapping[$key] ?? strtoupper($key);
return $_ENV[$envKey] ?? null;
}
public function getMetadata(): array
{
return ['type' => 'environment'];
}
}
// Usage
$envSource = new EnvSource('environment', [
'api-key' => 'APP_API_KEY',
'api-secret' => 'APP_API_SECRET',
]);

Combine multiple source types for powerful resolution:

use Cline\Cascade\Cascade;
use Cline\Cascade\Source\{CallbackSource, ArraySource, CacheSource};
// Customer database (cached)
$customerDb = new CallbackSource(
name: 'customer-db',
resolver: fn($key, $ctx) => $this->customerDb->find($ctx['customer_id'], $key),
supports: fn($key, $ctx) => isset($ctx['customer_id']),
);
$cachedCustomerDb = new CacheSource(
name: 'cached-customer',
inner: $customerDb,
cache: $this->cache,
ttl: 300,
);
// Platform API (cached)
$platformApi = new CallbackSource(
name: 'platform-api',
resolver: fn($key) => $this->platformApi->getConfig($key),
);
$cachedPlatformApi = new CacheSource(
name: 'cached-platform',
inner: $platformApi,
cache: $this->cache,
ttl: 600,
);
// Static defaults
$defaults = new ArraySource('defaults', [
'timeout' => 30,
'retries' => 3,
]);
// Build cascade
$cascade = Cascade::from()
->fallbackTo($cachedCustomerDb, priority: 1)
->fallbackTo($cachedPlatformApi, priority: 2)
->fallbackTo($defaults, priority: 3);