vuryss/doctrine-lazy-json-odm

A high-performance Doctrine JSON ODM with lazy loading using vuryss/serializer v2

v1.2.0 2025-08-21 10:55 UTC

This package is auto-updated.

Last update: 2025-08-21 10:55:25 UTC


README

A high-performance Symfony bundle that provides a Doctrine JSON ODM (Object Document Mapper) with advanced lazy loading capabilities. This bundle stores complex object structures as JSON in database fields while providing transparent lazy loading to improve performance.

PHP Version

Features

  • 🚀 Lazy Loading: Uses PHP 8.4 lazy objects for deferred JSON deserialization
  • âš¡ Serializer: Uses vuryss/serializer v2 for maximum performance
  • 🎯 Type Mapping: Map class names to aliases for storage optimization
  • 📦 Collection Support: Lazy-loaded arrays of objects with transparent usage
  • 📊 Performance Optimized: Significant memory and CPU savings for unused data

Requirements

  • PHP 8.4 or higher
  • Symfony 7.0+
  • Doctrine ORM 3.4+

Installation

Install the bundle via Composer:

composer require vuryss/doctrine-lazy-json-odm

Register the bundle in your config/bundles.php:

return [
    // ...
    \Vuryss\DoctrineLazyJsonOdm\DoctrineLazyJsonOdmBundle::class => ['all' => true],
];

Configuration

Create a configuration file config/packages/doctrine_lazy_json_odm.yaml:

doctrine_json_odm:
    type_map:
        user: 'App\Entity\User'
        product: 'App\Entity\Product'
        order: 'App\Entity\Order'
    cache_pool: 'app.doctrine_lazy_json_odm.cache_pool'

Usage

Basic Entity Definition

Define your entities with JSON ODM fields using the LazyJsonDocumentType:

<?php

use Doctrine\ORM\Mapping as ORM;
use Vuryss\DoctrineLazyJsonOdm\Lazy\LazyJsonArray;
use Vuryss\DoctrineLazyJsonOdm\Type\LazyJsonDocumentType;

#[ORM\Entity]
#[ORM\Table(name: 'users')]
class User
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private ?int $id = null;

    #[ORM\Column(type: 'string', length: 255)]
    private string $name;

    #[ORM\Column(type: 'string', length: 255, unique: true)]
    private string $email;

    // Single object stored as JSON with lazy loading
    #[ORM\Column(type: LazyJsonDocumentType::NAME, nullable: true)]
    private ?Profile $profile = null;

    // Array of objects stored as JSON with lazy loading
    #[ORM\Column(type: LazyJsonDocumentType::NAME, nullable: true)]
    private ?LazyJsonArray $orders = null;

    // Getters and setters...
}

Value Objects for JSON Storage

Create readonly classes for your JSON-stored objects:

<?php

readonly class Profile
{
    public function __construct(
        public string $firstName,
        public string $lastName,
        public int $age,
        public ?string $bio = null,
        public ?Address $address = null,
        public array $socialLinks = [],
    ) {
    }

    public function getFullName(): string
    {
        return $this->firstName . ' ' . $this->lastName;
    }
}

readonly class Address
{
    public function __construct(
        public string $street,
        public string $city,
        public string $postalCode,
        public string $country,
        public ?string $state = null,
    ) {
    }
}

Working with Entities

<?php

// Create and persist entities
$address = new Address('123 Main St', 'New York', '10001', 'USA');
$profile = new Profile('John', 'Doe', 30, 'Software Developer', $address);

$user = new User('John Doe', 'john@example.com');
$user->setProfile($profile);

$entityManager->persist($user);
$entityManager->flush();

// Retrieve and access lazy-loaded data
$user = $userRepository->find(1);

// Profile is lazy-loaded only when accessed
$fullName = $user->getProfile()->getFullName(); // Triggers lazy loading
$city = $user->getProfile()->address->city; // Nested objects work transparently

Working with Lazy Arrays

<?php

use Vuryss\DoctrineLazyJsonOdm\Lazy\LazyJsonArray;

// Create array of objects
$orderItems = [
    new OrderItem(1, 'Laptop', 1, '999.99', '999.99'),
    new OrderItem(2, 'Mouse', 1, '29.99', '29.99'),
];

$user->setOrders(new LazyJsonArray($orderItems));
$entityManager->flush();

// Access lazy array
$orders = $user->getOrders(); // LazyJsonArray instance
$firstOrder = $orders[0]; // Lazy loading triggered here
$orderCount = $orders->count(); // Available without loading items

// Iterate over lazy array
foreach ($user->getOrders() as $order) {
    echo $order->productName . "\n";
}

Advanced Configuration

Configure type aliases for optimized storage:

# config/packages/doctrine_lazy_json_odm.yaml
doctrine_json_odm:
    type_map:
        # Short aliases reduce JSON size
        user: 'App\Entity\User'
        profile: 'App\Entity\Profile'
        address: 'App\Entity\Address'
        order: 'App\Entity\Order'
        order_item: 'App\Entity\OrderItem'

        # Namespace-based aliases for organization
        catalog.product: 'App\Entity\Product'
        catalog.category: 'App\Entity\Category'

        # Version-specific aliases
        user_v2: 'App\Entity\UserV2'

    # Optional: Configure cache pool for metadata caching
    cache_pool: 'app.doctrine_lazy_json_odm.cache_pool'

Performance Optimization

<?php

// Use type aliases to reduce JSON size
$typeMap = [
    'u' => User::class,
    'p' => Profile::class,
    'a' => Address::class,
];

// JSON with full class names: ~200 bytes
// {"#type":"App\\Entity\\Profile","firstName":"John",...}

// JSON with aliases: ~150 bytes
// {"#type":"p","firstName":"John",...}

// Access only what you need to benefit from lazy loading
$user = $userRepository->find(1);
$name = $user->getName(); // No JSON deserialization yet
$profileName = $user->getProfile()->getFullName(); // Profile loaded here

// Lazy arrays
$orders = $user->getOrders(); // No loading yet
$orderCount = $orders->count(); // Loading all orders
$firstOrder = $orders[0]; // Already loaded

Testing

Run the test suite:

# Run all tests
vendor/bin/pest

Performance Considerations

Memory Efficiency

  • Lazy Loading: Objects are only deserialized when accessed, saving memory for unused data
  • Type Aliases: Short aliases can reduce JSON size significantly
  • Selective Access: Access only the properties you need

Best Practices

  1. Use Type Aliases: Configure short, meaningful aliases for frequently used classes
  2. Cache Configuration: Configure metadata caching for optimal performance

Benchmarks

// Without lazy loading: ~50MB memory for 10,000 user profiles
// With lazy loading: ~5MB memory (90% reduction)

// JSON size comparison:
// Full class names: {"#type":"App\\Entity\\Profile",...} (~200 bytes)
// Type aliases: {"#type":"p",...} (~140 bytes) - 30% smaller

Code Quality

The bundle maintains high code quality standards:

# PHP CS Fixer
vendor/bin/php-cs-fixer fix --using-cache=no

# PHPStan
vendor/bin/phpstan analyse -v

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for your changes
  4. Ensure all tests pass
  5. Run code quality tools
  6. Submit a pull request

License

This bundle is released under the MIT License. See the LICENSE file for details.

Credits