danilovl/object-to-array-transform-bundle
Symfony bundle that converts objects to arrays using configurable field mappings. Define fields in parameters and provide corresponding getters on your entities. Works with PHP 8.5+ and Symfony 8+.
Installation:
composer require danilovl/object-to-array-transform-bundle
Register the bundle in config/bundles.php:
Danilovl\ObjectToArrayTransformBundle\ObjectToArrayTransformBundle::class => ['all' => true]
Basic Configuration:
Define field mappings in config/services.yaml:
parameters:
api_fields:
default:
App\Entity\Shop:
fields:
- id
- name
- city
First Usage: Inject the service in a controller and transform an object:
use Danilovl\ObjectToArrayTransformBundle\Interfaces\ObjectToArrayTransformServiceInterface;
class ShopController extends AbstractController {
public function __construct(private ObjectToArrayTransformServiceInterface $transformer) {}
public function index(Shop $shop) {
$array = $this->transformer->transform('api_fields.default', $shop);
return new JsonResponse($array);
}
}
config/services.yaml for field mappings.ObjectToArrayTransformServiceInterface for method signatures.getX() methods.Transform a Doctrine entity into a minimal API response:
# config/services.yaml
parameters:
api_fields:
default:
App\Entity\User:
fields:
- id
- email
- roles
// Controller
$userArray = $this->transformer->transform('api_fields.default', $user);
Define Fields: Use YAML to map entities to fields, including nested objects:
parameters:
api_fields:
default:
App\Entity\Order:
fields:
- id
- customer:
fields:
- id
- name
- items:
fields:
- product
- quantity
Transform Objects:
Inject the service and call transform() with the config key and object:
$orderArray = $this->transformer->transform('api_fields.default', $order);
Handle Custom Logic: Override default behavior with parameters:
parameters:
api_fields:
default:
App\Entity\Product:
fields:
- id
- name
- isAvailable:
parameters:
method: 'getStockStatus'
Bulk Transformations: Use in loops or with Doctrine repositories:
$users = $this->userRepository->findAll();
$userArrays = array_map(
fn($user) => $this->transformer->transform('api_fields.default', $user),
$users
);
Dynamic Config Keys: Pass runtime keys for environment-specific transformations:
# config/services.yaml
parameters:
api_fields:
production:
App\Entity\Shop: { fields: [id, name] }
staging:
App\Entity\Shop: { fields: [id, name, city] }
$configKey = $_ENV['APP_ENV'] === 'production' ? 'production' : 'staging';
$shopArray = $this->transformer->transform("api_fields.$configKey", $shop);
DTOs: Transform entities to DTOs before serialization:
$dto = new ShopDto($shop);
$array = $this->transformer->transform('api_fields.default', $dto);
API Platform: Use alongside API Platform for hybrid approaches:
# config/api_platform/resources.yaml
resources:
App\Entity\Shop:
collectionOperations:
get:
normalization_context:
groups: ['shop:read']
itemOperations:
get:
normalization_context:
groups: ['shop:read']
# config/services.yaml
parameters:
api_fields:
default:
App\Entity\Shop:
fields:
- id
- name
- city
Conditional Fields: Use custom methods to include/exclude fields dynamically:
parameters:
api_fields:
default:
App\Entity\Order:
fields:
- id
- customer
- shippingAddress:
parameters:
method: 'getShippingAddressIfReady'
Date Formatting: Standardize datetime outputs:
parameters:
api_fields:
default:
parameters:
date_format: 'Y-m-d\TH:i:sP'
App\Entity\Event:
fields:
- id
- name
- startsAt
- endsAt
Circular References:
Handle self-referential entities with ignore_circular:
parameters:
api_fields:
default:
App\Entity\User:
fields:
- id
- name
- friends:
parameters:
ignore_circular: true
Missing Getters:
Undefined method 'getNonExistentField'.getX() methods. Use IDE autocompletion to verify.Circular References:
User → Orders → User).ignore_circular: true.Nested Field Typos:
city.address when address doesn’t exist).var_dump($transformer->transform(...)) to debug paths.Configuration Overrides:
api_fields.production, api_fields.staging).Performance with Deep Nesting:
Enable Debug Mode: Symfony’s debug toolbar shows the transformed array structure.
Log Config: Dump the config to verify mappings:
$config = $this->container->getParameter('api_fields');
file_put_contents('debug/config_dump.json', json_encode($config, JSON_PRETTY_PRINT));
Test Edge Cases:
null or defaults.new ArrayCollection().Field Order: YAML lists preserve order, but nested fields may not. Use explicit keys for consistency:
fields:
id: ~
name: ~
city: { fields: [id, name] }
Boolean Fields:
Methods returning bool may serialize as 1/0. Use custom methods to return strings:
fields:
isActive:
parameters:
method: 'getActiveStatus' # Returns "active"/"inactive"
Parameters Merge:
Nested parameters override parent config:
parameters:
date_format: 'Y-m-d'
App\Entity\Event:
fields:
createdAt:
parameters:
format: 'Y-m-d H:i:s' # Overrides parent
Custom Transformers:
Implement Danilovl\ObjectToArrayTransformBundle\Interfaces\ObjectToArrayTransformerInterface for non-standard objects (e.g., arrays, non-getter-accessible classes).
Pre/Post Processing: Use Symfony’s event system to modify arrays before/after transformation:
// src/EventListener/TransformListener.php
use Danilovl\ObjectToArrayTransformBundle\Event\TransformEvent;
class TransformListener {
public function onTransform(TransformEvent $event) {
$event->setArray(array_merge($event->getArray(), ['custom_field' => 'value']));
}
}
Dynamic Field Resolution:
Override Danilovl\ObjectToArrayTransformBundle\Service\ObjectToArrayTransformService to resolve fields at runtime (e.g., based on user roles).
Alternative Config Sources: Load fields from databases or APIs by extending the service to accept dynamic config providers.
If porting to Laravel:
Service Container:
Replace ParameterBag with Laravel’s config:
// config/api_fields.php
return [
'default' => [
'App\Models\Shop' => ['fields' => ['id', 'name']],
],
];
Service Binding: Bind the transformer in `AppServiceProvider
How can I help you explore Laravel packages today?