A Symfony bundle that provides a typed view layer for JSON API responses. Define response shapes as PHP classes, return them from controllers, and let the bundle handle serialization automatically — no manual JsonResponse construction needed.
Built for Symfony 8.0 and PHP 8.5+, the bundle eliminates boilerplate in REST API controllers by introducing view models with automatic property binding, collection mapping, and production-ready cache warming.
- Typed view models — define JSON response structures as PHP classes with typed properties
- Automatic property binding —
BindViewmaps domain object properties to view properties via reflection - Collection mapping —
IterableViewtransforms arrays and iterables with typed element views - Null stripping — null values are automatically excluded from serialized JSON output
- Build-time cache warming — pre-computed metadata and property mappings eliminate reflection overhead in production
- Build-versioned caching — cache files are tied to
container.build_idfor zero-downtime deployments - Doctrine proxy support — transparent lazy-load initialization before property access
- PHP 8.5+
- Symfony 8.0 components (http-kernel, serializer, property-access, dependency-injection, config, framework-bundle)
- doctrine/common ^3.5
composer require chamber-orchestra/view-bundle:8.0.*Enable the bundle in config/bundles.php:
return [
// ...
ChamberOrchestra\ViewBundle\ChamberOrchestraViewBundle::class => ['all' => true],
];Define a view model that maps properties from a domain object:
use ChamberOrchestra\ViewBundle\Attribute\BindsFrom;
use ChamberOrchestra\ViewBundle\Attribute\Type;
use ChamberOrchestra\ViewBundle\View\BindView;
use ChamberOrchestra\ViewBundle\View\IterableView;
#[BindsFrom(User::class)]
final class UserView extends BindView
{
public string $id;
public string $name;
#[Type(ImageView::class)]
public IterableView $images;
public function __construct(User $user)
{
parent::__construct($user);
}
}
final class ImageView extends BindView
{
public string $path;
}Return the view from a controller — the bundle converts it to a JsonResponse automatically:
#[Route('/user/me', methods: ['GET'])]
final class GetMeAction
{
public function __invoke(): UserView
{
return new UserView($this->getUser());
}
}ViewSubscriber converts any ViewInterface result into a JsonResponse. Non-view results pass through unchanged.
| View | Purpose |
|---|---|
ResponseView |
Base response with HTTP status (200) and Content-Type: application/json headers |
DataView |
Wraps any view or array under a "data" key |
BindView |
Maps matching properties from a source object using reflection |
IterableView |
Maps collections via a callback or view class string |
KeyValueView |
Produces associative array output for metadata blocks |
BindView uses BindUtils to synchronize properties between source objects and view instances. It handles:
- Built-in PHP types and custom objects
ViewInterfacesubclasses (auto-constructed)IterableViewproperties with#[Type(ViewClass::class)]attribute for typed collections- Skips union types and incompatible type pairs
- SetVersionSubscriber (priority 256) — injects the DI-managed
BindUtilsinstance intoBindViewviaBindView::setBindUtils() - Controller returns a
ViewInterfaceobject - ViewSubscriber — detects
ViewInterfaceresults, wraps non-ResponseViewInterfaceinDataView, serializes to JSON viaViewNormalizer
Views implementing ViewInterface are automatically tagged with chamber_orchestra.view via #[AutoconfigureTag]. The ViewPass compiler pass collects these classes and passes them to cache warmers for pre-computation.
The bundle includes a two-phase optimization strategy for production environments:
ViewMetadataFactorycaches property metadata in memory- Direct property access eliminates repeated reflection calls
- 30-50% faster normalization on repeated calls
ViewMetadataCacheWarmerpre-computes view property metadata at build timeBindUtilsCacheWarmerpre-computes property mappings (uses#[BindsFrom]for targeted source classes, falls back to N² pairs)- Generated opcache-optimized PHP files stored in
kernel.share_dir - Cache files are versioned with
container.build_idfor safe deployments - 60-80% reduction in reflection overhead on production requests
- Automatic fallback to reflection when warmed cache is unavailable
BindUtils is registered as a DI service with $buildId, $debug, and $shareDir constructor arguments. When APP_DEBUG=false, property accessor caching is enabled with a 24-hour lifetime. SetVersionSubscriber injects the configured instance into BindView on each request.
Warm the cache in production:
bin/console cache:warmup --env=prodThis generates build-versioned files in the shared cache directory:
- View property metadata (nullability, defaults, types)
- View-to-view property mappings for
BindUtils
PHPBench benchmarks are included to measure serialization performance and cache impact:
composer bench # Run all benchmarks
vendor/bin/phpbench run --report=default # Run with default reportBenchmark classes: BindUtilsBench, CacheWarmupBench, NormalizationBench.
composer install # Install dependencies
composer test # Run all tests (93 tests, 328 assertions)
./bin/phpunit # Run tests directly
./bin/phpunit --filter X # Run specific test class or methodMIT