orchestra/parser
Orchestra Parser is a lightweight PHP package from the Orchestra Platform that turns structured data formats into usable PHP arrays through a simple, extensible API, making it easy to read and transform configuration or markup in Laravel applications.
Install with the framework, not around it. The package's majors track Laravel majors, so v10 is presumptively Laravel 12 territory.
composer require orchestra/parser:^10.0
Do not float it (* or dev-main). Pin the major, commit the lockfile, and treat the next bump as a line item on the framework upgrade checklist — not something you discover when composer update breaks CI.
First thing to read is src/, not the README. The surface is deliberately small — a Parser contract, an auto-discovered service provider that binds it into the container, and pluggable handlers per format. Ten minutes in src/ will tell you more than any docs, and it settles the question that actually matters: what formats are registered out of the box. The package blurb advertises XML/YAML/INI/JSON/CSV via a unified parse()/format() API, while some internal write-ups describe it as an XML-only (or even PHP-AST) tool. Confirm against composer.json dependencies and the concrete handler list before anyone designs around it.
First use case — parse at build time, not request time.
use Orchestra\Parser\Parser;
// Resolve through the container; the provider is auto-discovered.
$parser = app(Parser::class);
$contents = file_get_contents(resource_path('blueprints/user.yaml'));
$blueprint = $parser->parse($contents); // shape: structured array/object
$asJson = $parser->format($blueprint, 'json'); // convert between formats
If that compiles a blueprint, manifest, stub, or definition into a reusable object, you are squarely in the package's intended lane. If you are parsing a request body or a user-supplied config, you are not — use json_decode, symfony/yaml, or SimpleXMLElement directly and skip the abstraction tax.
Wrap it in an application-owned adapter. This is the single highest-value pattern given the package's size and activity level.
namespace App\Support;
use Orchestra\Parser\Parser;
final class BlueprintCompiler
{
public function __construct(private Parser $parser) {}
/** @return array<string, mixed> */
public function compile(string $path): array
{
return once(fn () => $this->parser->parse(file_get_contents($path)));
}
}
Your app talks to BlueprintCompiler, never to Orchestra\Parser\Parser. When the next major bumps a constraint or shifts output shape, exactly one file changes.
Compile once, cache the result. Parsing is I/O + CPU with no caching of its own. Do it at deploy (php artisan app:compile-blueprints) and persist the structured result — a serialized array, a generated PHP class, or a Laravel config cache file. Never re-parse the same artifact on every request.
Use it for code generation and scaffolding. This is where it earns its keep day-to-day: definition files → generated classes, stubs → rendered templates, manifests → registered providers. Pair it with artisan commands so output is reproducible and diffable.
Golden-file tests are non-negotiable. Store fixtures in tests/Fixtures/parser/ and assert on the exact structure, not a subset:
public function test_user_blueprint_compiles(): void
{
$this->assertSame(
require __DIR__.'/Fixtures/expected/user_blueprint.php',
app(BlueprintCompiler::class)->compile(resource_path('blueprints/user.yaml')),
);
}
assertSame with a full expected array is what catches silent output drift — the failure mode that ships broken generated code without anyone noticing.
Swap it out in tests.
$this->instance(Parser::class, new class implements Parser {
public function parse($content) { return ['id' => 'stub']; }
public function format($data, $format) { return '{}'; }
});
Because you injected the contract, this is a two-liner.
Single-item vs. list shape changes. The classic parser trap: a repeated element with one occurrence may come back as a scalar, with two occurrences as an array. Guard generated code with normalization (Arr::wrap) or golden-file tests will not save you — they'll just fail loudly in prod when the count changes.
Everything is a string until you say otherwise. Parsed values arrive untyped. Cast at the boundary ((int), filter_var($v, FILTER_VALIDATE_BOOLEAN)) inside your adapter, once, rather than sprinkling casts at call sites.
XML specifics worth testing explicitly: attributes vs. child nodes, namespaces, CDATA sections, self-closing tags, and whitespace around text nodes. All four behave differently across parsers and are where "it worked locally" comes from.
XXE. If you parse XML from anywhere you don't fully control, verify external entity loading is disabled. Check the underlying loader configuration — do not assume.
Error handling is per-handler, so wrap defensively. Malformed input throws, and the exception type may differ by format. Catch broadly at the adapter boundary and rethrow as a domain exception so callers get one predictable failure.
Don't route config or JSON parsing through it. If the answer is json_decode or config(), this package adds an indirection and a version constraint for nothing. Same for reflection — use ReflectionClass.
Vendor risk is real but bounded. Zero dependents and sparse release notes mean there is no downstream community to diagnose breakage for you, and no external validation of the upgrade path. Small blast radius if it fails, small safety net when it does — which is exactly why the adapter and the pinned constraint matter more here than they would with a mainstream package.
Extension points: custom handlers are the escape hatch when you need a format the package doesn't ship. Register it rather than forking; forking a thinly-maintained package is how you end up owning it permanently.
Upgrade ritual, every framework cycle: branch → bump the major → composer why orchestra/parser → full test suite → golden-file diff → stage → deploy with lockfile rollback ready. It's a small, repeating cost; budget it as such rather than pretending it's a one-off.
How can I help you explore Laravel packages today?