如何在Symfony中用Doctrine将嵌入对象存储为JSONB?
Absolutely! You don't need to split your NestedObject into separate database columns—Doctrine lets you handle this cleanly with custom database types, which will automatically serialize/deserialize your nested object to/from PostgreSQL's jsonb format. Here's a step-by-step solution:
1. Create a Custom Doctrine Type
First, build a custom type that extends Doctrine's built-in JsonType and handles the conversion between your NestedObject instance and JSON data.
Option A: Use Symfony Serializer (Recommended for Nested Structures)
This approach works seamlessly even if NestedObject has its own nested objects (like FirstOption and SecondOption):
// src/Doctrine/Type/NestedObjectType.php <?php namespace App\Doctrine\Type; use App\Entity\NestedObject; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Types\JsonType; use Symfony\Component\Serializer\SerializerInterface; class NestedObjectType extends JsonType { private SerializerInterface $serializer; public function __construct(SerializerInterface $serializer) { $this->serializer = $serializer; } public function convertToDatabaseValue($value, PostgreSQLPlatform $platform) { if ($value instanceof NestedObject) { // Serialize the object to a JSON string return $this->serializer->serialize($value, 'json'); } return parent::convertToDatabaseValue($value, $platform); } public function convertToPHPValue($value, PostgreSQLPlatform $platform) { $jsonData = parent::convertToPHPValue($value, $platform); if (is_string($jsonData)) { // Deserialize JSON back to a NestedObject instance return $this->serializer->deserialize($jsonData, NestedObject::class, 'json'); } return $jsonData; } public function getName() { // Unique identifier for your custom type return 'nested_object'; } public function requiresSQLCommentHint(PostgreSQLPlatform $platform) { return true; } }
Option B: Manual Serialization (For Simple Objects)
If your NestedObject is straightforward, you can use PHP's magic serialization methods instead of the Symfony Serializer:
First, add __serialize() and __unserialize() to your NestedObject:
// src/Entity/NestedObject.php class NestedObject { private $firstOption; private $secondOption; // ... existing getters public function __serialize(): array { return [ 'firstOption' => $this->firstOption, 'secondOption' => $this->secondOption, ]; } public function __unserialize(array $data): void { $this->firstOption = $data['firstOption']; $this->secondOption = $data['secondOption']; } }
Then simplify the custom type:
// src/Doctrine/Type/NestedObjectType.php public function convertToDatabaseValue($value, PostgreSQLPlatform $platform) { if ($value instanceof NestedObject) { return json_encode($value); } return parent::convertToDatabaseValue($value, $platform); } public function convertToPHPValue($value, PostgreSQLPlatform $platform) { $jsonData = parent::convertToPHPValue($value, $platform); if (is_string($jsonData)) { $rawObject = json_decode($jsonData); $nestedObject = new NestedObject(); $nestedObject->__unserialize((array)$rawObject); return $nestedObject; } return $jsonData; }
2. Register the Custom Type
Tell Doctrine about your new type by adding it to your Doctrine configuration:
# config/packages/doctrine.yaml doctrine: dbal: # ... your existing database connection settings types: nested_object: App\Doctrine\Type\NestedObjectType
3. Update Your Entity Annotation
Modify your Entity class to use the custom type and explicitly specify jsonb for PostgreSQL:
// src/Entity/Entity.php /** * @ORM\Entity(repositoryClass="App\Repository\EntityRepository") */ class Entity { // ... existing $id property /** * @ORM\Column(type="nested_object", options={"jsonb": true}) * * @var NestedObject */ private $nestedObject; // ... existing getter/setter methods remain unchanged }
4. Configure Symfony Serializer (If Using Option A)
Ensure the Serializer can access your object's properties by adding this to your framework config:
# config/packages/framework.yaml framework: serializer: enable_annotations: true property_access: magic_getters: true # Uses your existing getters for serialization magic_setters: true # Uses setters (if you add them) for deserialization
If your nested objects (like FirstOption) need specific serialization rules, you can add @SerializedName or @Groups annotations to their properties.
Alternative: Lifecycle Callbacks
If you don't want to create a custom type, you can use Doctrine lifecycle callbacks to handle serialization manually:
// src/Entity/Entity.php /** * @ORM\Entity(repositoryClass="App\Repository\EntityRepository") * @ORM\HasLifecycleCallbacks() */ class Entity { // ... properties /** * @ORM\PrePersist * @ORM\PreUpdate */ public function serializeNestedObject(): void { $this->nestedObject = json_encode($this->nestedObject); } /** * @ORM\PostLoad */ public function deserializeNestedObject(): void { $rawData = json_decode($this->nestedObject, true); $nestedObject = new NestedObject(); // Manually populate properties (use reflection for cleaner code if needed) // Example: // $nestedObject->setFirstOption($rawData['firstOption']); // ... $this->nestedObject = $nestedObject; } }
This approach is less elegant than custom types, as you have to handle the conversion logic directly in your entity.
With the custom type approach, you can use your NestedObject just like any other property in your entity—Doctrine will automatically handle converting it to jsonb when saving to the database, and back to an object when querying.
内容的提问来源于stack exchange,提问作者Roman Andreev

