Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Placeholder Resolver Laravel Package

dflydev/placeholder-resolver

A lightweight PHP library for resolving placeholders such as %key% in strings using array or custom data sources. It supports recursive substitution, nested lookups, and circular-reference detection, making it useful for configuration and templating.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require dflydev/placeholder-resolver

The two classes that matter

  • Dflydev\PlaceholderResolver\DataSource\DataSourceInterface — the "where do values come from" abstraction (exists($name) / get($name)).
  • Dflydev\PlaceholderResolver\Resolver\Resolver — walks ${name} tokens, asks the data source, and recurses into the returned value.

If you learn only these two, you've learned the package.

Minimal working example

use Dflydev\PlaceholderResolver\DataSource\ArrayDataSource;
use Dflydev\PlaceholderResolver\Resolver\Resolver;

$source = new ArrayDataSource([
    'app.host' => 'example.test',
    'app.port' => '8080',
    'app.url'  => 'https://${app.host}:${app.port}',
]);

$resolver = new Resolver($source);

echo $resolver->resolvePlaceholder('${app.url}');
// https://example.test:8080

The magic is the recursion: app.url resolves to a string containing two more placeholders, and the resolver keeps going until the string is fully expanded.

Where to look first

  1. README.md — the API surface is tiny and fully documented there.
  2. src/Resolver/Resolver.php — the actual token regex, recursion loop, and circular-reference handling. Read this before you build anything non-trivial.
  3. src/DataSource/ — see what data sources ship with the package before writing your own.

First use case: a JSON/YAML config file with internal references

This is the sweet spot. A tenant settings file, a feature-flag manifest, or a config/services.json where you don't want to repeat the same host and port five times:

{
  "host": "api.example.test",
  "port": 443,
  "base_url": "https://${host}:${port}",
  "webhook_url": "${base_url}/hooks"
}

Decode it, feed it to ArrayDataSource, resolve the keys you care about, then config()->set(...) or hand it to your settings service. No custom string parsing, no accidental infinite loops.

Implementation Patterns

1. A config-backed data source (the Laravel default)

ArrayDataSource uses literal key lookups. If you want dot-nested access to Laravel's config repository, wrap it:

namespace App\Support;

use Dflydev\PlaceholderResolver\DataSource\DataSourceInterface;

class ConfigDataSource implements DataSourceInterface
{
    public function exists($name)
    {
        return config()->has($name);
    }

    public function get($name)
    {
        return config($name);
    }
}

Now ${app.url}, ${database.connections.mysql.host}, and ${services.stripe.key} all work without flattening anything.

2. Composition: request data → config → env

The interface is deliberately trivial, so layering sources is easy:

class ChainDataSource implements DataSourceInterface
{
    /** @var DataSourceInterface[] */
    private array $sources;

    public function __construct(array $sources)
    {
        $this->sources = $sources;
    }

    public function exists($name)
    {
        foreach ($this->sources as $source) {
            if ($source->exists($name)) {
                return true;
            }
        }

        return false;
    }

    public function get($name)
    {
        foreach ($this->sources as $source) {
            if ($source->exists($name)) {
                return $source->get($name);
            }
        }

        return null;
    }
}

Order is precedence: first match wins. This is how you get tenant overrides (tenant settings → app config → env) without touching your templates.

3. Bind a resolver singleton in a service provider

There is no Laravel auto-discovery — register it yourself and inject the interface, not the concrete class:

use Dflydev\PlaceholderResolver\Resolver\PlaceholderResolverInterface;
use Dflydev\PlaceholderResolver\Resolver\Resolver;

public function register()
{
    $this->app->singleton(PlaceholderResolverInterface::class, function () {
        return new Resolver(new \App\Support\ConfigDataSource());
    });
}

Injecting PlaceholderResolverInterface keeps tests honest: in tests, swap in a resolver backed by ArrayDataSource and you never touch config().

4. Bulk resolution at boot, not per request

If you have a resolved settings tree, resolve it once at boot and cache the result in the container or config():

$raw = json_decode(file_get_contents(base_path('config/tenant.json')), true);

$resolver = new Resolver(new ArrayDataSource($raw));

$settings = $resolver->resolvePlaceholders($raw); // keeps keys

config(['tenant' => $settings]);

Doing this in register() means it participates in php artisan config:cache and you pay the cost once.

5. Runtime templates from the database

Where this genuinely beats str_replace: user- or admin-authored templates stored in a settings table, e.g. email subjects, webhook bodies, invoice footers:

$template = Setting::get('invoice.footer');
// "Thanks for your order, ${customer.name}. Support: ${support.email}"

echo $resolver->resolvePlaceholder($template);

Back the data source with a per-request array built from the Customer model and your support config. You get nested resolution and cycle detection for free.

6. Decorator for logging and caching

Because DataSourceInterface is two methods, it's a natural decorator seam. Wrap it to log misses, cache DB lookups, or redact secret keys during local debugging — without touching the resolver or your call sites.

7. Treat the resolver as a boundary in your domain

If your application has a "settings" or "template" concept, depend on PlaceholderResolverInterface rather than sprinkling preg_replace calls through the codebase. It gives you one place to change delimiter handling, one place to add observability, and one thing to mock.

Gotchas and Tips

Missing keys are silent. If the data source reports exists() === false, the resolver leaves the ${...} token untouched rather than throwing. That means a typo in ${custmer.name} ships to production as a literal. After resolving admin-authored templates, assert the output no longer contains ${ — or scan for it in tests.

Circular references blow up. a => ${b}, b => ${a} (or a => ${a}) raises a CircularReferenceException from the resolver namespace (verify the exact FQCN in src/Resolver for your installed version). Catch it at boot and fail fast — an exception during config load is far better than a half-resolved URL in production. Add a unit test with a deliberate cycle.

ArrayDataSource does not understand dot notation. It is a flat lookup. ['app' => ['url' => ...]] will not satisfy ${app.url}. Either flatten with Arr::dot() or use a config-backed data source like the one above.

Values are assumed to be strings. If get() returns an array or object, the resolver's string replacement will produce "Array to string conversion" notices and garbage. Make your data source cast scalars and explicitly reject/handle non-scalars. true/false/null deserve thought too — decide whether they become "1", "", or stay unresolved.

There is no escape mechanism. Any ${...} in the input is treated as a placeholder. If templates come from users, they can reference config keys they shouldn't see — a real secret-leak vector if the resolver is backed by ConfigDataSource. Allowlist the keys you expose (a dedicated data source with a fixed map is safer than the whole config repository) or strip ${ from user input.

Don't cache mutable resolutions. If you memoize resolved values, remember that the data source is live. A resolver cached on a singleton holding request-specific data (the current customer, the current tenant) will leak across requests under Octane. Build the data source per request, or key your cache on the tenant/user.

Resolution is lazy and order-independent within a data source. ${base_url} can be defined before or after ${host}; the data source is queried on demand. But this only holds for values inside the data source. If you resolve a plain PHP array element by element without the source containing the referenced keys, cross-references won't work. Put the whole tree in the data source first.

Old package, thin maintenance. It's stable and small, but it predates modern PHP: no scalar type hints, no strict types, and the API may predate PHP 8. It works, but check the composer.json PHP constraint before upgrading to a new PHP version, and don't expect typed signatures in your IDE without docblocks.

Performance is per-string recursion. Fine for a few hundred config keys at boot; wrong for resolving every row of a paginated result set. Resolve once, cache, and keep the resolver out of hot loops.

Testing is trivial — exploit it. ArrayDataSource plus Resolver needs no framework, no container, and no database. For any feature that involves templating, write plain PHPUnit tests against the resolver directly; reserve Laravel-level tests for the service-provider wiring.

Extension points: implement DataSourceInterface (DB, Vault, feature flags, request context), decorate an existing source for caching/logging, and subclass Resolver only if you need a different token pattern — check whether the class is final in your version first.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings