pragmarx/yaml
PragmaRX YAML is a lightweight Laravel and PHP package for parsing and dumping YAML. It wraps Symfony's YAML component with a simple service provider and facade, making it easy to load YAML config or translation files, convert YAML strings to arrays, and export arrays back to YAML.
Install and verify it's active
composer require pragmarx/yaml
php artisan config:clear
On Laravel 5.5+ the service provider is auto-discovered. On older apps, register PragmaRX\Yaml\ServiceProvider (check the package's composer.json extra.laravel.providers key for the exact class name in your installed version) in config/app.php.
Where to look first
LoadConfiguration bootstrap for one that also scans config/ for *.yaml / *.yml files. Understanding that override is understanding the package.vendor/pragmarx/yaml/src/ — it's small; read it end-to-end before relying on it in production.First use case
Take one static, deeply nested PHP config file and convert it. Good candidates: permission maps, menu trees, feature flags, pricing tables — anything with structure but no closures or env() calls.
# config/permissions.yaml
roles:
admin:
- users.create
- users.delete
editor:
- posts.create
- posts.update
// anywhere
config('permissions.roles.editor'); // ['posts.create', 'posts.update']
Verify with php artisan tinker → config('permissions'). If it returns null, the file wasn't picked up: check the extension, the filename (permissions.yaml must map to the permissions config key), and that the provider is actually registered (php artisan about on newer Laravel, or dump app()->getLoadedProviders()).
Additive, not replacement. The sane production pattern is: PHP files keep anything dynamic (env(), config() calls, class names, closures); YAML files carry static structure. Mixing both in one logical config namespace is fine as long as you know the merge order — verify whether YAML wins over an equally-named .php file, and never ship both.
Structure-heavy config. YAML shines for nested arrays and sequences. Laravel flattens any config array to dot access, so design YAML with the dot path you'll actually type in mind:
# config/features.yaml
billing:
invoices:
enabled: true
providers:
- stripe
- paddle
→ config('features.billing.invoices.providers.1').
Config-driven loops. Sequences become zero-indexed arrays, which is the natural fit for foreach over "things to register":
webhooks:
- event: order.created
url: https://example.com/hooks/orders
Then a service provider reads config('webhooks') and registers listeners. This keeps the "what" declarative and version-controlled, and the "how" in code.
Manual loading when the loader is too coarse. If you want YAML somewhere other than config/ (e.g. resources/data/), or want environment-specific YAML, skip the package's bootstrap override and parse directly with the underlying symfony/yaml:
$data = \Symfony\Component\Yaml\Yaml::parseFile(
resource_path("data/pricing.{$env}.yaml"),
\Symfony\Component\Yaml\Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE
);
config(['pricing' => array_replace_recursive(config('pricing', []), $data)]);
Do this in a service provider's register() so it runs before anything reads the config.
CI/CD and config caching. php artisan config:cache serialises the resolved array, so cached deployments never touch the YAML parser. Treat YAML edits exactly like PHP config edits: rebuild the cache in your deploy step, always. In tests, prefer config()->set() over mutating YAML fixtures — it's faster and keeps the parser out of your test path.
Migration strategy. Convert one file per PR, keep the PHP version in git history, and diff the resolved array:
// before/after
dd(config('permissions'), require config_path('permissions.php'));
That diff is your entire safety net for this package. Use it.
No PHP execution inside YAML. This is the headline limitation. No env(), no config(), no closures, no ::class. Any config that varies per environment must stay in PHP or be handled by the manual-load pattern above with a filename convention. Do not attempt to fake env substitution with placeholder strings — there's no built-in interpolation here.
YAML 1.1 type coercion bites. Unquoted yes, no, on, off, y, n parse as booleans. Unquoted 1.10 becomes a float 1.1. Leading-zero strings lose their zeros. Dates may parse as DateTime depending on flags. Quote anything that is semantically a string: "yes", "1.10", "0123".
Indentation errors are the whole debugging story. Tabs are forbidden; mixing tabs and spaces throws. Symfony's parser gives you a line number in the ParseException — read it, don't guess. Fast isolated check without booting Laravel:
php -r "var_dump(Symfony\Component\Yaml\Yaml::parseFile('config/app.yaml'));"
Name collisions. A config/foo.yaml and config/foo.php both targeting the foo key will merge in whatever order the loader decides — this is precisely the kind of thing that silently works in dev and inverts in a later Laravel version. Pick one format per config key, always.
Silent failures when cached. With bootstrap/cache/config.php present, a broken YAML file is invisible: nothing parses it. A missing-file or syntax bug will only surface in an uncached environment. Run php artisan config:clear locally before claiming something works, and add a config:cache step to CI to catch it.
Package staleness is your real risk. Last release May 2020, ~114 stars, zero dependents. Expect the service provider to reference framework internals (LoadConfiguration, FileLoader, container bootstrapping) that have shifted since Laravel 8/9. Check composer why-not laravel/framework ^10 / ^11 before adopting, and expect to either constrain your Laravel version, add a Composer replace/fork under a VCS repo, or pin symfony/yaml to a compatible major. Audit the provider against your Laravel version's LoadConfiguration in vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/ — the diff tells you if it will break.
Debugging checklist, in order: is the provider loaded → does the filename map to the expected config key → does the YAML parse in isolation → is the config cache stale → is a same-named .php file winning.
Extension points. The service provider is the seam: extend or replace its loader class if you need a different search path, extra .yml handling, or pre-parse transformation. Because the package is small and unmaintained, subclassing the provider in your own app is usually more durable than waiting for upstream — and cheaper than a hard fork.
How can I help you explore Laravel packages today?