├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Bundle │ ├── DependencyInjection │ │ ├── Configuration.php │ │ └── DunglasDoctrineJsonOdmExtension.php │ ├── DunglasDoctrineJsonOdmBundle.php │ └── Resources │ │ └── config │ │ └── services.xml ├── Serializer.php ├── SerializerTrait.php ├── Type │ └── JsonDocumentType.php ├── TypeMapper.php ├── TypeMapperInterface.php └── TypedSerializerTrait.php └── tests ├── AbstractKernelTestCase.php ├── Fixtures ├── AppKernel.php ├── AppKernelWithCustomTypeMapper.php ├── AppKernelWithTypeMap.php └── TestBundle │ ├── CustomTypeMapper.php │ ├── DependencyInjection │ ├── InjectCustomNormalizerPass.php │ └── MakeServicesPublicPass.php │ ├── Document │ ├── Attribute.php │ ├── Attributes.php │ ├── Bar.php │ ├── Baz.php │ ├── ScalarValue.php │ ├── ScalarValueTrait.php │ ├── TypedScalarValueTrait.php │ └── WithMappedType.php │ ├── Entity │ ├── Foo.php │ └── Product.php │ ├── Enum │ └── InputMode.php │ └── TestBundle.php ├── FunctionalTest.php ├── SerializerTest.php └── bootstrap.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [dunglas] 2 | tidelift: "packagist/dunglas/doctrine-json-odm" 3 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: "tests" 2 | 3 | on: 4 | pull_request: ~ 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | run: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | php-version: 14 | - "8.1" 15 | - "8.2" 16 | - "8.3" 17 | - "8.4" 18 | dependencies: 19 | - highest 20 | include: 21 | - php-version: "8.4" 22 | dependencies: lowest 23 | fail-fast: false 24 | env: 25 | SYMFONY_DEPRECATIONS_HELPER: ${{ matrix.dependencies == 'lowest' && 'disabled=1' || 'max[direct]=0' }} 26 | 27 | services: 28 | mysql: 29 | image: mysql:8 30 | ports: 31 | - 3306:3306 32 | options: --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 10 33 | env: 34 | MYSQL_ROOT_PASSWORD: root 35 | MYSQL_DATABASE: odm 36 | 37 | postgres: 38 | image: postgres:14 39 | ports: 40 | - 5432:5432 41 | options: --health-cmd "/usr/bin/pg_isready" --health-interval 10s --health-timeout 5s --health-retries 10 42 | env: 43 | POSTGRES_PASSWORD: postgres 44 | POSTGRES_DB: odm 45 | 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v4 49 | 50 | - name: Setup PHP 51 | uses: shivammathur/setup-php@v2 52 | with: 53 | php-version: ${{ matrix.php-version }} 54 | extensions: mbstring, xml, ctype, iconv, intl, pdo_mysql, pdo_pgsql 55 | 56 | - name: Install dependencies 57 | uses: ramsey/composer-install@v3 58 | with: 59 | dependency-versions: "${{ matrix.dependencies }}" 60 | 61 | - name: Install PHPUnit 62 | run: php vendor/bin/simple-phpunit install 63 | 64 | - name: Run tests (MySQL) 65 | env: 66 | DATABASE_URL: mysql://root:root@127.0.0.1:3306/odm?serverVersion=8.0 67 | run: php vendor/bin/simple-phpunit 68 | 69 | - name: Run tests (PostgreSQL) 70 | env: 71 | DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/odm?serverVersion=14&charset=utf8 72 | run: php vendor/bin/simple-phpunit 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | /phpunit.xml 4 | /.phpunit.result.cache 5 | /tests/Fixtures/cache 6 | /tests/Fixtures/logs 7 | /var 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## MySQL 4 | 5 | To execute the test suite, you need a running MySQL server. 6 | 7 | The easiest way to get them up and running is using Docker: 8 | 9 | docker run --rm --platform=linux/amd64 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=odm -p 3306:3306 -d mysql 10 | 11 | Then run the test suite: 12 | 13 | DATABASE_URL='mysql://root:root@127.0.0.1:3306/odm?serverVersion=8.0' ./vendor/bin/simple-phpunit 14 | 15 | ## Postgres 16 | 17 | To execute the test suite, you need a running PostgreSQL server. 18 | 19 | The easiest way to get them up and running is using Docker: 20 | 21 | docker run --rm -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres 22 | 23 | Then run the test suite: 24 | 25 | DATABASE_URL='postgresql://postgres:postgres@127.0.0.1:5432/postgres?serverVersion=14&charset=utf8' ./vendor/bin/simple-phpunit 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Kévin Dunglas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doctrine JSON ODM 2 | 3 | An Object-Document Mapper (ODM) for [Doctrine ORM](http://www.doctrine-project.org/projects/orm.html) leveraging new JSON types of modern RDBMS. 4 | 5 | [![tests](https://github.com/dunglas/doctrine-json-odm/actions/workflows/tests.yml/badge.svg)](https://github.com/dunglas/doctrine-json-odm/actions/workflows/tests.yml) 6 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/dunglas/doctrine-json-odm/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/dunglas/doctrine-json-odm/?branch=master) 7 | [![StyleCI](https://styleci.io/repos/57223826/shield)](https://styleci.io/repos/57223826) 8 | 9 | Did you ever dream of a tool creating powerful data models mixing traditional, efficient relational mappings with modern 10 | schema-less and NoSQL-like ones? 11 | 12 | With Doctrine JSON ODM, it's now possible to create and query such hybrid data models with ease. Thanks to [modern JSON 13 | types of RDBMS](http://www.postgresql.org/docs/current/static/datatype-json.html), querying schema-less documents is easy, 14 | powerful and [fast as hell (similar in performance to a MongoDB database)](http://www.enterprisedb.com/postgres-plus-edb-blog/marc-linster/postgres-outperforms-mongodb-and-ushers-new-developer-reality)! 15 | You can even [define indexes](http://www.postgresql.org/docs/current/static/datatype-json.html#JSON-INDEXING) for those documents. 16 | 17 | Doctrine JSON ODM allows to store PHP objects as JSON documents in modern, dynamic columns of an RDBMS. 18 | It works with JSON and JSONB columns of PostgreSQL (>= 9.4) and the JSON column type of MySQL (>= 5.7.8). 19 | 20 | For more information about concepts behind Doctrine JSON ODM, take a look at [the presentation given by Benjamin Eberlei at Symfony Catalunya 2016](https://www.youtube.com/watch?v=E8w1y1Jo7YI). 21 | 22 | ## Install 23 | 24 | To install the library, use [Composer](https://getcomposer.org/), the PHP package manager: 25 | 26 | composer require dunglas/doctrine-json-odm 27 | 28 | If you are using [Symfony](https://symfony.com) or [API Platform](https://api-platform.com), you don't need to do anything else! 29 | If you use Doctrine directly, use a bootstrap code similar to the following: 30 | 31 | ```php 32 | setSerializer( 51 | new Serializer([new BackedEnumNormalizer(), new UidNormalizer(), new DateTimeNormalizer(), new ArrayDenormalizer(), new ObjectNormalizer()], [new JsonEncoder()]) 52 | ); 53 | } 54 | 55 | // Sample bootstrapping code here, adapt to fit your needs 56 | $isDevMode = true; 57 | $config = Setup::createAnnotationMetadataConfiguration([__DIR__ . '/../src'], $_ENV['DEBUG'] ?? false); // Adapt to your path 58 | 59 | $conn = [ 60 | 'dbname' => $_ENV['DATABASE_NAME'], 61 | 'user' => $_ENV['DATABASE_USER'], 62 | 'password' => $_ENV['DATABASE_PASSWORD'], 63 | 'host' => $_ENV['DATABASE_HOST'], 64 | 'driver' => 'pdo_mysql' // or pdo_pgsql 65 | ]; 66 | 67 | return EntityManager::create($conn, $config); 68 | ``` 69 | 70 | ## Usage 71 | 72 | Doctrine JSON ODM provides a `json_document` column type for properties of Doctrine entities. 73 | 74 | The content of properties mapped with this type is serialized in JSON using the [Symfony Serializer](http://symfony.com/doc/current/components/serializer.html) 75 | then, it is stored in a dynamic JSON column in the database. 76 | 77 | When the object will be hydrated, the JSON content of this column is transformed back to its original values, thanks again 78 | to the Symfony Serializer. 79 | All PHP objects and structures will be preserved. 80 | 81 | You can store any type of (serializable) PHP data structures in properties mapped using the `json_document` type. 82 | 83 | Example: 84 | 85 | ```php 86 | namespace App\Entity; 87 | 88 | use Doctrine\ORM\Mapping\{Entity, Column, Id, GeneratedValue}; 89 | 90 | // This is a typical Doctrine ORM entity. 91 | #[Entity] 92 | class Foo 93 | { 94 | #[Column] 95 | #[Id] 96 | #[GeneratedValue] 97 | public int $id; 98 | 99 | #[Column] 100 | public string $name; 101 | 102 | // Can contain anything: array, objects, nested objects... 103 | #[Column(type: 'json_document', options: ['jsonb' => true])] 104 | public $misc; 105 | 106 | // Works with private and protected methods with getters and setters too. 107 | } 108 | ``` 109 | 110 | ```php 111 | namespace App\Entity; 112 | 113 | // This is NOT an entity! It's a POPO (Plain Old PHP Object). It can contain anything. 114 | class Bar 115 | { 116 | public string $title; 117 | public float $weight; 118 | } 119 | ``` 120 | 121 | ```php 122 | namespace App\Entity; 123 | 124 | // This is NOT an entity. It's another POPO and it can contain anything. 125 | class Baz 126 | { 127 | public string $name; 128 | public int $size; 129 | } 130 | ``` 131 | 132 | Store a graph of random object in the JSON type of the database: 133 | 134 | ```php 135 | // $entityManager = $managerRegistry->getManagerForClass(Foo::class); 136 | 137 | $bar = new Bar(); 138 | $bar->title = 'Bar'; 139 | $bar->weight = 12.3; 140 | 141 | $baz = new Baz(); 142 | $baz->name = 'Baz'; 143 | $baz->size = 7; 144 | 145 | $foo = new Foo(); 146 | $foo->name = 'Foo'; 147 | $foo->misc = [$bar, $baz]; 148 | 149 | $entityManager->persist($foo); 150 | $entityManager->flush(); 151 | ``` 152 | 153 | Retrieve the object graph back: 154 | 155 | ```php 156 | $foo = $entityManager->find(Foo::class, $foo->getId()); 157 | var_dump($foo->misc); // Same as what we set earlier 158 | ``` 159 | 160 | ### Using type aliases 161 | 162 | Using custom type aliases as `#type` rather than FQCNs has a couple of benefits: 163 | - In case you move or rename your document classes, you can just update your type map without migrating database content 164 | - For applications that might store millions of records with JSON documents, this can also save some storage space 165 | 166 | You can introduce type aliases at any point in time. Already persisted JSON documents with class names will still get deserialized correctly. 167 | 168 | #### Using Symfony 169 | 170 | In order to use type aliases, add the bundle configuration, e.g. in `config/packages/doctrine_json_odm.yaml`: 171 | 172 | ```yaml 173 | dunglas_doctrine_json_odm: 174 | type_map: 175 | foo: App\Something\Foo 176 | bar: App\SomethingElse\Bar 177 | ``` 178 | 179 | With this, `Foo` objects will be serialized as: 180 | 181 | ```json 182 | { "#type": "foo", "someProperty": "someValue" } 183 | ``` 184 | 185 | Another option is to use your own custom type mapper implementing `Dunglas\DoctrineJsonOdm\TypeMapperInterface`. For this, just override the service definition: 186 | 187 | ```yaml 188 | services: 189 | dunglas_doctrine_json_odm.type_mapper: '@App\Something\MyFancyTypeMapper' 190 | ``` 191 | 192 | #### Without Symfony 193 | 194 | When instantiating `Dunglas\DoctrineJsonOdm\Serializer`, you need to pass an extra argument that implements `Dunglas\DoctrineJsonOdm\TypeMapperInterface`. 195 | 196 | For using the built-in type mapper: 197 | 198 | ```php 199 | // … 200 | use Dunglas\DoctrineJsonOdm\Serializer; 201 | use Dunglas\DoctrineJsonOdm\TypeMapper; 202 | use App\Something\Foo; 203 | use App\SomethingElse\Bar; 204 | 205 | // For using the built-in type mapper: 206 | $typeMapper = new TypeMapper([ 207 | 'foo' => Foo::class, 208 | 'bar' => Bar::class, 209 | ]); 210 | 211 | // Or implement TypeMapperInterface with your own class: 212 | $typeMapper = new MyTypeMapper(); 213 | 214 | // Then pass it into the Serializer constructor 215 | Type::getType('json_document')->setSerializer( 216 | new Serializer([new ArrayDenormalizer(), new ObjectNormalizer()], [new JsonEncoder()], $typeMapper) 217 | ); 218 | ``` 219 | 220 | ### Limitations when updating nested properties 221 | 222 | Due to how Doctrine works, it will not detect changes to nested objects or properties. 223 | The reason for this is that Doctrine compares objects by reference to optimize `UPDATE` queries. 224 | If you experience problems where no `UPDATE` queries are executed, you might need to `clone` the object before you set it. 225 | That way Doctrine will notice the change. See https://github.com/dunglas/doctrine-json-odm/issues/21 for more information. 226 | 227 | ## FAQ 228 | 229 | **What DBMS are supported?** 230 | 231 | PostgreSQL 9.4+ and MySQL 5.7+ are supported. 232 | 233 | **Which versions of Doctrine are supported?** 234 | 235 | Doctrine ORM 2.6+ and DBAL 2.6+ are supported. 236 | 237 | **How to use [the JSONB type of PostgreSQL](http://www.postgresql.org/docs/current/static/datatype-json.html)?** 238 | 239 | Then, you need to set an option in the column mapping: 240 | 241 | ```php 242 | // ... 243 | 244 | #[Column(type: 'json_document', options: ['jsonb' => true])] 245 | public $foo; 246 | 247 | // ... 248 | ``` 249 | 250 | **Does the ODM support nested objects and object graphs?** 251 | 252 | Yes. 253 | 254 | **Can I use the native [PostgreSQL](http://www.postgresql.org/docs/current/static/datatype-json.html) and [MySQL](https://dev.mysql.com/doc/refman/en/json.html) /JSON functions?** 255 | 256 | Yes! You can execute complex queries using [native queries](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/native-sql.html). 257 | 258 | Alternatively, install [scienta/doctrine-json-functions](https://github.com/ScientaNL/DoctrineJsonFunctions) to be able to use run JSON functions in DQL and query builders. 259 | 260 | **How to change the (de)serialization context** 261 | 262 | You may need to change the (de)serialization context, for instance to avoid escaping slashes. 263 | 264 | If you are using Symfony, modify your Kernel like this: 265 | 266 | ```php 267 | setSerializationContext([JsonEncode::OPTIONS => JSON_UNESCAPED_SLASHES]); 289 | $type->setDeserializationContext([/* ... */]); 290 | } 291 | } 292 | ``` 293 | 294 | **How can I add additional normalizers?** 295 | 296 | The Symfony Serializer is easily extensible. This bundle registers and uses a service with ID `dunglas_doctrine_json_odm.serializer` as the serializer for the JSON type. 297 | This means we can easily override it in our `services.yaml` to use additional normalizers. 298 | As an example we inject a custom normalizer service. Be aware that the order of the normalizers might be relevant depending on the normalizers you use. 299 | 300 | ```yaml 301 | # Add DateTime Normalizer to Dunglas' Doctrine JSON ODM Bundle 302 | dunglas_doctrine_json_odm.serializer: 303 | class: Dunglas\DoctrineJsonOdm\Serializer 304 | arguments: 305 | - ['@App\MyCustom\Normalizer', '@?dunglas_doctrine_json_odm.normalizer.backed_enum', '@?dunglas_doctrine_json_odm.normalizer.uid', '@dunglas_doctrine_json_odm.normalizer.datetime', '@dunglas_doctrine_json_odm.normalizer.array', '@dunglas_doctrine_json_odm.normalizer.object'] 306 | - ['@serializer.encoder.json'] 307 | - '@?dunglas_doctrine_json_odm.type_mapper' 308 | public: true 309 | autowire: false 310 | autoconfigure: false 311 | ``` 312 | 313 | **When the namespace of a used entity changes** 314 | 315 | For classes without [type aliases](#using-type-aliases), because we store the `#type` along with the data in the database, you have to migrate the already existing data in your database to reflect the new namespace. 316 | 317 | Example: If we have a project that we migrate from `AppBundle` to `App`, we have the namespace `AppBundle/Entity/Bar` in our database which has to become `App/Entity/Bar` instead. 318 | 319 | When you use `MySQL`, you can use this query to migrate the data: 320 | ```sql 321 | UPDATE Baz 322 | SET misc = JSON_REPLACE(misc, '$."#type"', 'App\\\Entity\\\Bar') 323 | WHERE 'AppBundle\\\Entity\\\Bar' = JSON_EXTRACT(misc, '$."#type"'); 324 | ``` 325 | 326 | ## Credits 327 | 328 | This bundle is brought to you by [Kévin Dunglas](https://dunglas.dev) and [awesome contributors](https://github.com/dunglas/doctrine-json-odm/graphs/contributors). 329 | Sponsored by [Les-Tilleuls.coop](https://les-tilleuls.coop). 330 | 331 | ## Former Maintainers 332 | 333 | [Yanick Witschi](https://github.com/Toflar) helped maintain this bundle, thanks! 334 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Security updates are included in new releases only. 6 | They **are not backported** to previous versions. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Use Tidelift to report a security issue affecting this package: https://tidelift.com/security 11 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dunglas/doctrine-json-odm", 3 | "type": "library", 4 | "description": "An object document mapper for Doctrine ORM using JSON types of modern RDBMS.", 5 | "keywords": ["ORM", "ODM", "JSON", "database", "RDBMS", "PostgreSQL", "MySQL"], 6 | "homepage": "https://dunglas.fr", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Kévin Dunglas", 11 | "email": "kevin@dunglas.fr", 12 | "homepage": "https://dunglas.fr" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=8.1", 17 | "doctrine/orm": "^2.6.3 || ^3.0.0", 18 | "symfony/property-access": "^5.4 || ^6.0 || ^7.0", 19 | "symfony/property-info": "^5.4 || ^6.0 || ^7.0", 20 | "symfony/serializer": "^5.4 || ^6.0 || ^7.0" 21 | }, 22 | "require-dev": { 23 | "doctrine/annotations": "^1.0 || ^2.0.0", 24 | "doctrine/doctrine-bundle": "^1.12.13 || ^2.2", 25 | "doctrine/dbal": "^2.7 || ^3.3 || ^4.0.0", 26 | "symfony/finder": "^5.4 || ^6.0 || ^7.0", 27 | "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0", 28 | "symfony/phpunit-bridge": "^6.0 || ^7.0", 29 | "symfony/uid": "^5.4 || ^6.0 || ^7.0", 30 | "symfony/validator": "^5.4 || ^6.0 || ^7.0" 31 | }, 32 | "suggest": { 33 | "symfony/framework-bundle": "To use the provided bundle.", 34 | "scienta/doctrine-json-functions": "To add support for JSON functions in DQL." 35 | }, 36 | "autoload": { 37 | "psr-4": { "Dunglas\\DoctrineJsonOdm\\": "src/" } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { "Dunglas\\DoctrineJsonOdm\\Tests\\": "tests/" } 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-main": "1.0.x-dev" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | . 6 | 7 | 8 | tests 9 | vendor 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | tests 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Bundle/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Bundle\DependencyInjection; 11 | 12 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 13 | use Symfony\Component\Config\Definition\ConfigurationInterface; 14 | 15 | final class Configuration implements ConfigurationInterface 16 | { 17 | /** 18 | * @return TreeBuilder 19 | */ 20 | public function getConfigTreeBuilder(): TreeBuilder 21 | { 22 | $treeBuilder = new TreeBuilder('dunglas_doctrine_json_odm'); 23 | 24 | $treeBuilder->getRootNode() 25 | ->children() 26 | ->arrayNode('type_map') 27 | ->defaultValue([]) 28 | ->useAttributeAsKey('type') 29 | ->scalarPrototype() 30 | ->cannotBeEmpty() 31 | ->validate() 32 | ->ifTrue(static function (string $v): bool { 33 | return !class_exists($v); 34 | }) 35 | ->thenInvalid('Use fully qualified classnames as type values') 36 | ->end() 37 | ->end() 38 | ->end() 39 | ->end(); 40 | 41 | return $treeBuilder; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Bundle/DependencyInjection/DunglasDoctrineJsonOdmExtension.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Bundle\DependencyInjection; 11 | 12 | use Symfony\Component\Config\FileLocator; 13 | use Symfony\Component\DependencyInjection\ContainerBuilder; 14 | use Symfony\Component\DependencyInjection\Extension\Extension; 15 | use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; 16 | use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; 17 | use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; 18 | use Symfony\Component\Serializer\Normalizer\UidNormalizer; 19 | 20 | /** 21 | * @author Kévin Dunglas 22 | */ 23 | final class DunglasDoctrineJsonOdmExtension extends Extension implements PrependExtensionInterface 24 | { 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function prepend(ContainerBuilder $container): void 29 | { 30 | if (empty($frameworkConfiguration = $container->getExtensionConfig('framework'))) { 31 | return; 32 | } 33 | 34 | if (!isset($frameworkConfiguration['serializer']['enabled'])) { 35 | $container->prependExtensionConfig('framework', ['serializer' => ['enabled' => true]]); 36 | } 37 | 38 | if (!isset($frameworkConfiguration['property_info']['enabled'])) { 39 | $container->prependExtensionConfig('framework', ['property_info' => ['enabled' => true]]); 40 | } 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function load(array $configs, ContainerBuilder $container): void 47 | { 48 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 49 | $loader->load('services.xml'); 50 | 51 | if (PHP_VERSION_ID < 80100 || !class_exists(BackedEnumNormalizer::class)) { 52 | $container->removeDefinition('dunglas_doctrine_json_odm.normalizer.backed_enum'); 53 | } 54 | 55 | if (!class_exists(UidNormalizer::class)) { 56 | $container->removeDefinition('dunglas_doctrine_json_odm.normalizer.uid'); 57 | } 58 | 59 | $config = $this->processConfiguration(new Configuration(), $configs); 60 | 61 | if ($config['type_map'] ?? []) { 62 | $container->getDefinition('dunglas_doctrine_json_odm.type_mapper')->addArgument($config['type_map']); 63 | } else { 64 | $container->removeDefinition('dunglas_doctrine_json_odm.type_mapper'); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Bundle/DunglasDoctrineJsonOdmBundle.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Bundle; 11 | 12 | use Doctrine\DBAL\Types\Type; 13 | use Dunglas\DoctrineJsonOdm\Type\JsonDocumentType; 14 | use Symfony\Component\HttpKernel\Bundle\Bundle; 15 | 16 | /** 17 | * Doctrine JSON ODM integration with the Symfony framework. 18 | * 19 | * @author Kévin Dunglas 20 | */ 21 | final class DunglasDoctrineJsonOdmBundle extends Bundle 22 | { 23 | public function __construct() 24 | { 25 | if (!Type::hasType('json_document')) { 26 | Type::addType('json_document', JsonDocumentType::class); 27 | } 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function boot(): void 34 | { 35 | $type = Type::getType('json_document'); 36 | $type->setSerializer($this->container->get('dunglas_doctrine_json_odm.serializer')); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Bundle/Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | null 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/Serializer.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm; 11 | 12 | use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; 13 | use Symfony\Component\Serializer\Serializer as BaseSerializer; 14 | 15 | if (method_exists(ArrayDenormalizer::class, 'setSerializer')) { 16 | // Symfony <=5.4 17 | final class Serializer extends BaseSerializer 18 | { 19 | use SerializerTrait; 20 | 21 | private const KEY_TYPE = '#type'; 22 | private const KEY_SCALAR = '#scalar'; 23 | } 24 | } else { 25 | // Symfony >=6.0 26 | final class Serializer extends BaseSerializer 27 | { 28 | use TypedSerializerTrait; 29 | 30 | private const KEY_TYPE = '#type'; 31 | private const KEY_SCALAR = '#scalar'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/SerializerTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm; 11 | 12 | use Symfony\Component\Serializer\Encoder\DecoderInterface; 13 | use Symfony\Component\Serializer\Encoder\EncoderInterface; 14 | use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; 15 | use Symfony\Component\Serializer\Normalizer\NormalizerInterface; 16 | 17 | /** 18 | * @internal 19 | * 20 | * @author Kévin Dunglas 21 | */ 22 | trait SerializerTrait 23 | { 24 | /** 25 | * @var TypeMapperInterface|null 26 | */ 27 | private $typeMapper; 28 | 29 | /** 30 | * @param (NormalizerInterface|DenormalizerInterface)[] $normalizers 31 | * @param (EncoderInterface|DecoderInterface)[] $encoders 32 | */ 33 | public function __construct(array $normalizers = [], array $encoders = [], ?TypeMapperInterface $typeMapper = null) 34 | { 35 | parent::__construct($normalizers, $encoders); 36 | 37 | $this->typeMapper = $typeMapper; 38 | } 39 | 40 | /** 41 | * @param mixed $data 42 | * @param string|null $format 43 | * 44 | * @return array|\ArrayObject|bool|float|int|string|null 45 | */ 46 | public function normalize($data, $format = null, array $context = []) 47 | { 48 | $normalizedData = parent::normalize($data, $format, $context); 49 | 50 | if (\is_object($data)) { 51 | $typeName = \get_class($data); 52 | 53 | if ($this->typeMapper) { 54 | $typeName = $this->typeMapper->getTypeByClass($typeName); 55 | } 56 | 57 | $typeData = [self::KEY_TYPE => $typeName]; 58 | $valueData = is_scalar($normalizedData) ? [self::KEY_SCALAR => $normalizedData] : $normalizedData; 59 | $normalizedData = array_merge($typeData, $valueData); 60 | } 61 | 62 | return $normalizedData; 63 | } 64 | 65 | /** 66 | * @param $data 67 | * 68 | * @return mixed 69 | */ 70 | public function denormalize($data, $type, $format = null, array $context = []) 71 | { 72 | if (\is_array($data) && (isset($data[self::KEY_TYPE]))) { 73 | $keyType = $data[self::KEY_TYPE]; 74 | 75 | if ($this->typeMapper) { 76 | $keyType = $this->typeMapper->getClassByType($keyType); 77 | } 78 | 79 | unset($data[self::KEY_TYPE]); 80 | 81 | $data = $data[self::KEY_SCALAR] ?? $data; 82 | $data = $this->denormalize($data, $keyType, $format, $context); 83 | 84 | return parent::denormalize($data, $keyType, $format, $context); 85 | } 86 | 87 | if (is_iterable($data)) { 88 | $type = ('' === $type) ? 'stdClass' : $type; 89 | 90 | return parent::denormalize($data, $type.'[]', $format, $context); 91 | } 92 | 93 | return $data; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Type/JsonDocumentType.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Type; 11 | 12 | use Doctrine\DBAL\Platforms\AbstractPlatform; 13 | use Doctrine\DBAL\Types\JsonType; 14 | use Symfony\Component\Serializer\SerializerInterface; 15 | 16 | /** 17 | * The JSON document type. 18 | * 19 | * @author Kévin Dunglas 20 | */ 21 | final class JsonDocumentType extends JsonType 22 | { 23 | public const NAME = 'json_document'; 24 | 25 | private SerializerInterface $serializer; 26 | 27 | private string $format = 'json'; 28 | 29 | private array $serializationContext = []; 30 | 31 | private array $deserializationContext = []; 32 | 33 | /** 34 | * Sets the serializer to use. 35 | */ 36 | public function setSerializer(SerializerInterface $serializer): void 37 | { 38 | $this->serializer = $serializer; 39 | } 40 | 41 | /** 42 | * Gets the serializer or throw an exception if it isn't available. 43 | * 44 | * @throws \RuntimeException 45 | */ 46 | private function getSerializer(): SerializerInterface 47 | { 48 | if (null === $this->serializer) { 49 | throw new \RuntimeException(sprintf('An instance of "%s" must be available. Call the "setSerializer" method.', SerializerInterface::class)); 50 | } 51 | 52 | return $this->serializer; 53 | } 54 | 55 | /** 56 | * Sets the serialization format (default to "json"). 57 | */ 58 | public function setFormat(string $format): void 59 | { 60 | $this->format = $format; 61 | } 62 | 63 | /** 64 | * Sets the serialization context (default to an empty array). 65 | */ 66 | public function setSerializationContext(array $serializationContext): void 67 | { 68 | $this->serializationContext = $serializationContext; 69 | } 70 | 71 | /** 72 | * Sets the deserialization context (default to an empty array). 73 | */ 74 | public function setDeserializationContext(array $deserializationContext): void 75 | { 76 | $this->deserializationContext = $deserializationContext; 77 | } 78 | 79 | public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string 80 | { 81 | if (null === $value) { 82 | return null; 83 | } 84 | 85 | return $this->getSerializer()->serialize($value, $this->format, $this->serializationContext); 86 | } 87 | 88 | public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed 89 | { 90 | if (null === $value || $value === '') { 91 | return null; 92 | } 93 | 94 | return $this->getSerializer()->deserialize($value, '', $this->format, $this->deserializationContext); 95 | } 96 | 97 | public function requiresSQLCommentHint(AbstractPlatform $platform): bool 98 | { 99 | return true; 100 | } 101 | 102 | public function getName(): string 103 | { 104 | return self::NAME; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/TypeMapper.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm; 11 | 12 | /** 13 | * Allows using string constants in place of class names. 14 | */ 15 | final class TypeMapper implements TypeMapperInterface 16 | { 17 | /** 18 | * @var array 19 | */ 20 | private $typeToClass; 21 | 22 | /** 23 | * @var array 24 | */ 25 | private $classToType; 26 | 27 | /** 28 | * @param array $typeToClass 29 | */ 30 | public function __construct(array $typeToClass) 31 | { 32 | $this->typeToClass = $typeToClass; 33 | $this->classToType = array_flip($typeToClass); 34 | } 35 | 36 | /** 37 | * Falls back to class name itself. 38 | * 39 | * @param class-string $class 40 | */ 41 | public function getTypeByClass(string $class): string 42 | { 43 | return $this->classToType[$class] ?? $class; 44 | } 45 | 46 | /** 47 | * Falls back to type name itself – it might as well be a class. 48 | * 49 | * @return class-string 50 | */ 51 | public function getClassByType(string $type): string 52 | { 53 | return $this->typeToClass[$type] ?? $type; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/TypeMapperInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm; 11 | 12 | /** 13 | * Allows using string constants in place of class names. 14 | */ 15 | interface TypeMapperInterface 16 | { 17 | /** 18 | * Resolve type name from class. 19 | * 20 | * @param class-string $class 21 | */ 22 | public function getTypeByClass(string $class): string; 23 | 24 | /** 25 | * Resolve class from type name. 26 | * 27 | * @return class-string 28 | */ 29 | public function getClassByType(string $type): string; 30 | } 31 | -------------------------------------------------------------------------------- /src/TypedSerializerTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm; 11 | 12 | /** 13 | * @internal 14 | * 15 | * @author Kévin Dunglas 16 | */ 17 | trait TypedSerializerTrait 18 | { 19 | use SerializerTrait { 20 | normalize as private doNormalize; 21 | denormalize as private doDenormalize; 22 | } 23 | 24 | public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null 25 | { 26 | return $this->doNormalize($data, $format, $context); 27 | } 28 | 29 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed 30 | { 31 | return $this->doDenormalize($data, $type, $format, $context); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/AbstractKernelTestCase.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests; 11 | 12 | use Symfony\Bundle\FrameworkBundle\Console\Application; 13 | use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; 14 | 15 | abstract class AbstractKernelTestCase extends KernelTestCase 16 | { 17 | /** 18 | * @var Application 19 | */ 20 | protected $application; 21 | 22 | protected function setUp(): void 23 | { 24 | self::bootKernel(); 25 | 26 | $this->application = new Application(self::$kernel); 27 | $this->application->setAutoExit(false); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Fixtures/AppKernel.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; 11 | use Dunglas\DoctrineJsonOdm\Bundle\DunglasDoctrineJsonOdmBundle; 12 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\DependencyInjection\MakeServicesPublicPass; 13 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\TestBundle; 14 | use Symfony\Bundle\FrameworkBundle\FrameworkBundle; 15 | use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; 16 | use Symfony\Component\Config\Loader\LoaderInterface; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\HttpKernel\Kernel; 19 | use Symfony\Component\Routing\RouteCollectionBuilder; 20 | 21 | /** 22 | * Test purpose micro-kernel. 23 | * 24 | * @author Kévin Dunglas 25 | */ 26 | class AppKernel extends Kernel 27 | { 28 | use MicroKernelTrait; 29 | 30 | public function registerBundles(): iterable 31 | { 32 | return [ 33 | new FrameworkBundle(), 34 | new DoctrineBundle(), 35 | new DunglasDoctrineJsonOdmBundle(), 36 | new TestBundle(), 37 | ]; 38 | } 39 | 40 | protected function configureRoutes(RouteCollectionBuilder $routes): void 41 | { 42 | } 43 | 44 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void 45 | { 46 | $container->loadFromExtension('framework', [ 47 | 'secret' => 'jsonodm', 48 | 'test' => null, 49 | 'http_method_override' => false, 50 | ]); 51 | 52 | $container->loadFromExtension('doctrine', [ 53 | 'dbal' => [ 54 | 'url' => '%env(resolve:DATABASE_URL)%', 55 | ], 56 | 'orm' => [ 57 | 'auto_generate_proxy_classes' => true, 58 | 'auto_mapping' => true, 59 | ], 60 | ]); 61 | 62 | // Make a few services public until we depend on Symfony 4.1+ and can use the new test container 63 | $container->addCompilerPass(new MakeServicesPublicPass()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Fixtures/AppKernelWithCustomTypeMapper.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures; 11 | 12 | use AppKernel; 13 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\CustomTypeMapper; 14 | use Symfony\Component\Config\Loader\LoaderInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Definition; 17 | 18 | class AppKernelWithCustomTypeMapper extends AppKernel 19 | { 20 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void 21 | { 22 | parent::configureContainer($container, $loader); 23 | 24 | $container->setDefinition('dunglas_doctrine_json_odm.type_mapper', new Definition(CustomTypeMapper::class)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Fixtures/AppKernelWithTypeMap.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures; 11 | 12 | use AppKernel; 13 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\WithMappedType; 14 | use Symfony\Component\Config\Loader\LoaderInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | 17 | class AppKernelWithTypeMap extends AppKernel 18 | { 19 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void 20 | { 21 | parent::configureContainer($container, $loader); 22 | 23 | $container->loadFromExtension('dunglas_doctrine_json_odm', [ 24 | 'type_map' => [ 25 | 'mappedType' => WithMappedType::class, 26 | ], 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/CustomTypeMapper.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle; 11 | 12 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\WithMappedType; 13 | use Dunglas\DoctrineJsonOdm\TypeMapperInterface; 14 | 15 | class CustomTypeMapper implements TypeMapperInterface 16 | { 17 | public function getTypeByClass(string $class): string 18 | { 19 | return $class === WithMappedType::class ? 'customTypeAlias' : $class; 20 | } 21 | 22 | public function getClassByType(string $type): string 23 | { 24 | return $type === 'customTypeAlias' ? WithMappedType::class : $type; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/DependencyInjection/InjectCustomNormalizerPass.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\DependencyInjection; 11 | 12 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 13 | use Symfony\Component\DependencyInjection\ContainerBuilder; 14 | use Symfony\Component\DependencyInjection\Definition; 15 | use Symfony\Component\DependencyInjection\Reference; 16 | use Symfony\Component\Serializer\Normalizer\CustomNormalizer; 17 | 18 | class InjectCustomNormalizerPass implements CompilerPassInterface 19 | { 20 | public function process(ContainerBuilder $container): void 21 | { 22 | $container->setDefinition('dunglas_doctrine_json_odm.normalizer.custom', new Definition(CustomNormalizer::class)); 23 | 24 | $serializerDefinition = $container->getDefinition('dunglas_doctrine_json_odm.serializer'); 25 | $arguments = $serializerDefinition->getArguments(); 26 | $arguments[0] = array_merge([new Reference('dunglas_doctrine_json_odm.normalizer.custom')], $arguments[0]); 27 | $serializerDefinition->setArguments($arguments); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/DependencyInjection/MakeServicesPublicPass.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\DependencyInjection; 11 | 12 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 13 | use Symfony\Component\DependencyInjection\ContainerBuilder; 14 | 15 | class MakeServicesPublicPass implements CompilerPassInterface 16 | { 17 | public function process(ContainerBuilder $container): void 18 | { 19 | static $services = [ 20 | 'doctrine.orm.default_entity_manager', 21 | 'doctrine.dbal.default_connection', 22 | 'doctrine', 23 | 'serializer', 24 | ]; 25 | 26 | foreach ($services as $service) { 27 | if (!$container->hasDefinition($service)) { 28 | continue; 29 | } 30 | 31 | $definition = $container->getDefinition($service); 32 | $definition->setPublic(true); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/Document/Attribute.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document; 11 | 12 | /** 13 | * @author Kévin Dunglas 14 | */ 15 | class Attribute 16 | { 17 | public $key; 18 | public $value; 19 | } 20 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/Document/Attributes.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document; 11 | 12 | class Attributes 13 | { 14 | private $attributes = []; 15 | 16 | public function getAttributes(): array 17 | { 18 | return $this->attributes; 19 | } 20 | 21 | public function setAttributes(array $attributes): void 22 | { 23 | $this->attributes = $attributes; 24 | } 25 | 26 | public function addAttributes(Attribute $attribute): void 27 | { 28 | $this->attributes[] = $attribute; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/Document/Bar.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document; 11 | 12 | /** 13 | * @author Kévin Dunglas 14 | */ 15 | class Bar 16 | { 17 | private $title; 18 | private $weight; 19 | 20 | /** 21 | * @return mixed 22 | */ 23 | public function getTitle() 24 | { 25 | return $this->title; 26 | } 27 | 28 | /** 29 | * @param mixed $title 30 | */ 31 | public function setTitle($title): void 32 | { 33 | $this->title = $title; 34 | } 35 | 36 | /** 37 | * @return mixed 38 | */ 39 | public function getWeight() 40 | { 41 | return $this->weight; 42 | } 43 | 44 | /** 45 | * @param mixed $weight 46 | */ 47 | public function setWeight($weight): void 48 | { 49 | $this->weight = $weight; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/Document/Baz.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document; 11 | 12 | /** 13 | * @author Kévin Dunglas 14 | */ 15 | class Baz 16 | { 17 | private $name; 18 | private $size; 19 | 20 | public function getName() 21 | { 22 | return $this->name; 23 | } 24 | 25 | public function setName($name): void 26 | { 27 | $this->name = $name; 28 | } 29 | 30 | public function getSize() 31 | { 32 | return $this->size; 33 | } 34 | 35 | public function setSize($size): void 36 | { 37 | $this->size = $size; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/Document/ScalarValue.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document; 11 | 12 | use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; 13 | use Symfony\Component\Serializer\Normalizer\DenormalizableInterface; 14 | use Symfony\Component\Serializer\Normalizer\NormalizableInterface; 15 | 16 | /** 17 | * Fixture class to test object normalized as scalar values. 18 | * 19 | * @author Antonio J. García Lagar 20 | */ 21 | if (method_exists(ArrayDenormalizer::class, 'setSerializer')) { 22 | // Symfony <=5.4 23 | class ScalarValue implements NormalizableInterface, DenormalizableInterface 24 | { 25 | use ScalarValueTrait; 26 | } 27 | } else { 28 | // Symfony >=6.0 29 | class ScalarValue implements NormalizableInterface, DenormalizableInterface 30 | { 31 | use ScalarValueTrait, TypedScalarValueTrait { 32 | TypedScalarValueTrait::normalize insteadof ScalarValueTrait; 33 | TypedScalarValueTrait::denormalize insteadof ScalarValueTrait; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/Document/ScalarValueTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document; 11 | 12 | use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; 13 | use Symfony\Component\Serializer\Normalizer\NormalizerInterface; 14 | 15 | trait ScalarValueTrait 16 | { 17 | private $value; 18 | 19 | public function __construct($value = '') 20 | { 21 | $this->value = $value; 22 | } 23 | 24 | public function value() 25 | { 26 | return $this->value; 27 | } 28 | 29 | public function normalize(NormalizerInterface $normalizer, $format = null, array $context = []) 30 | { 31 | return $this->value; 32 | } 33 | 34 | public function denormalize(DenormalizerInterface $denormalizer, $data, $format = null, array $context = []) 35 | { 36 | $this->value = $data; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/Document/TypedScalarValueTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document; 11 | 12 | use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; 13 | use Symfony\Component\Serializer\Normalizer\NormalizerInterface; 14 | 15 | trait TypedScalarValueTrait 16 | { 17 | public function normalize(NormalizerInterface $normalizer, ?string $format = null, array $context = []): array|string|int|float|bool 18 | { 19 | return $this->value; 20 | } 21 | 22 | public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, ?string $format = null, array $context = []): void 23 | { 24 | $this->value = $data; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/Document/WithMappedType.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document; 11 | 12 | class WithMappedType 13 | { 14 | public $foo = 'bar'; 15 | } 16 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/Entity/Foo.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Entity; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | /** 15 | * @ORM\Entity 16 | * 17 | * @author Kévin Dunglas 18 | */ 19 | #[ORM\Entity] 20 | class Foo 21 | { 22 | /** 23 | * @ORM\Column(type="integer") 24 | * 25 | * @ORM\Id 26 | * 27 | * @ORM\GeneratedValue(strategy="AUTO") 28 | */ 29 | #[ 30 | ORM\Column(type: 'integer'), 31 | ORM\Id, 32 | ORM\GeneratedValue(strategy: 'AUTO'), 33 | ] 34 | private $id; 35 | 36 | /** 37 | * @ORM\Column(type="string") 38 | */ 39 | #[ORM\Column(type: 'string')] 40 | private $name; 41 | 42 | /** 43 | * @ORM\Column(type="json_document", options={"jsonb": true}) 44 | */ 45 | #[ORM\Column(type: 'json_document', options: ['jsonb' => true])] 46 | private $misc; 47 | 48 | public function getId() 49 | { 50 | return $this->id; 51 | } 52 | 53 | public function getName() 54 | { 55 | return $this->name; 56 | } 57 | 58 | public function setName($name): void 59 | { 60 | $this->name = $name; 61 | } 62 | 63 | public function getMisc() 64 | { 65 | return $this->misc; 66 | } 67 | 68 | public function setMisc(array $misc): void 69 | { 70 | $this->misc = $misc; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/Entity/Product.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Entity; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | /** 15 | * @ORM\Entity 16 | * 17 | * @author Kévin Dunglas 18 | */ 19 | #[ORM\Entity] 20 | class Product 21 | { 22 | /** 23 | * @ORM\Column(type="integer") 24 | * 25 | * @ORM\Id 26 | * 27 | * @ORM\GeneratedValue(strategy="AUTO") 28 | */ 29 | #[ 30 | ORM\Column(type: 'integer'), 31 | ORM\Id, 32 | ORM\GeneratedValue(strategy: 'AUTO'), 33 | ] 34 | public $id; 35 | 36 | /** 37 | * @ORM\Column(type="string") 38 | */ 39 | #[ORM\Column(type: 'string')] 40 | public $name; 41 | 42 | /** 43 | * @ORM\Column(type="json_document", options={"jsonb": true}, nullable=true) 44 | */ 45 | #[ORM\Column(type: 'json_document', nullable: true, options: ['jsonb' => true])] 46 | public $attributes; 47 | } 48 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/Enum/InputMode.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Enum; 11 | 12 | enum InputMode: string 13 | { 14 | case EMAIL = 'email'; 15 | } 16 | -------------------------------------------------------------------------------- /tests/Fixtures/TestBundle/TestBundle.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle; 11 | 12 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\DependencyInjection\InjectCustomNormalizerPass; 13 | use Symfony\Component\DependencyInjection\ContainerBuilder; 14 | use Symfony\Component\HttpKernel\Bundle\Bundle; 15 | 16 | /** 17 | * @author Kévin Dunglas 18 | */ 19 | final class TestBundle extends Bundle 20 | { 21 | public function build(ContainerBuilder $container): void 22 | { 23 | $container->addCompilerPass(new InjectCustomNormalizerPass()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/FunctionalTest.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests; 11 | 12 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Attribute; 13 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Attributes; 14 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Bar; 15 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Baz; 16 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\ScalarValue; 17 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Entity\Foo; 18 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Entity\Product; 19 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Enum\InputMode; 20 | use Symfony\Component\Console\Input\StringInput; 21 | use Symfony\Component\Uid\Uuid; 22 | 23 | /** 24 | * @author Kévin Dunglas 25 | */ 26 | class FunctionalTest extends AbstractKernelTestCase 27 | { 28 | protected function setUp(): void 29 | { 30 | parent::setUp(); 31 | 32 | $this->runCommand('doctrine:schema:drop --force'); 33 | $this->runCommand('doctrine:schema:create'); 34 | } 35 | 36 | private function runCommand($command): void 37 | { 38 | $this->application->run(new StringInput($command.' --no-interaction --quiet')); 39 | } 40 | 41 | public function testStoreAndRetrieveDocument(): void 42 | { 43 | $attribute1 = new Attribute(); 44 | $attribute1->key = 'foo'; 45 | $attribute1->value = 'bar'; 46 | 47 | $attribute2 = new Attribute(); 48 | $attribute2->key = 'weights'; 49 | $attribute2->value = [34, 67]; 50 | 51 | $attributes = [$attribute1, $attribute2]; 52 | 53 | $product = new Product(); 54 | $product->name = 'My product'; 55 | $product->attributes = $attributes; 56 | 57 | $manager = self::$kernel->getContainer()->get('doctrine')->getManagerForClass(Product::class); 58 | $manager->persist($product); 59 | $manager->flush(); 60 | 61 | $manager->clear(); 62 | 63 | $retrievedProduct = $manager->find(Product::class, $product->id); 64 | $this->assertEquals($attributes, $retrievedProduct->attributes); 65 | } 66 | 67 | public function testStoreAndRetrieveDocumentsOfVariousTypes(): void 68 | { 69 | $bar = new Bar(); 70 | $bar->setTitle('Bar'); 71 | $bar->setWeight(12); 72 | 73 | $baz = new Baz(); 74 | $baz->setName('Baz'); 75 | $baz->setSize(7); 76 | 77 | $scalarValue = new ScalarValue('foobar'); 78 | 79 | $misc = [$bar, $baz, $scalarValue]; 80 | 81 | $foo = new Foo(); 82 | $foo->setName('Foo'); 83 | $foo->setMisc($misc); 84 | 85 | $manager = self::$kernel->getContainer()->get('doctrine')->getManagerForClass(Foo::class); 86 | $manager->persist($foo); 87 | $manager->flush(); 88 | 89 | $manager->clear(); 90 | 91 | $foo = $manager->find(Foo::class, $foo->getId()); 92 | $this->assertEquals($misc, $foo->getMisc()); 93 | } 94 | 95 | public function testNestedObjects(): void 96 | { 97 | $attribute = new Attribute(); 98 | $attribute->key = 'nested'; 99 | $attribute->value = 'bar'; 100 | 101 | $attributeParent = new Attribute(); 102 | $attributeParent->key = 'parent'; 103 | $attributeParent->value = [[$attribute]]; 104 | 105 | $misc = [$attributeParent]; 106 | 107 | $foo = new Foo(); 108 | $foo->setName('foo'); 109 | $foo->setMisc($misc); 110 | 111 | $manager = self::$kernel->getContainer()->get('doctrine')->getManagerForClass(Foo::class); 112 | $manager->persist($foo); 113 | $manager->flush(); 114 | 115 | $manager->clear(); 116 | 117 | $foo = $manager->find(Foo::class, $foo->getId()); 118 | $this->assertEquals($misc, $foo->getMisc()); 119 | } 120 | 121 | public function testNestedObjectsInNestedObject(): void 122 | { 123 | $attribute1 = new Attribute(); 124 | $attribute1->key = 'attribute1'; 125 | 126 | $attribute2 = new Attribute(); 127 | $attribute2->key = 'attribute2'; 128 | 129 | $attributes = new Attributes(); 130 | $attributes->setAttributes([$attribute1, $attribute2]); 131 | 132 | $misc = [$attributes]; 133 | 134 | $foo = new Foo(); 135 | $foo->setName('foo'); 136 | $foo->setMisc($misc); 137 | 138 | $manager = self::$kernel->getContainer()->get('doctrine')->getManagerForClass(Foo::class); 139 | 140 | $manager->persist($foo); 141 | $manager->flush(); 142 | $manager->clear(); 143 | 144 | $foo = $manager->find(Foo::class, $foo->getId()); 145 | 146 | $this->assertEquals($misc, $foo->getMisc()); 147 | } 148 | 149 | public function testNullIsStoredAsNull(): void 150 | { 151 | $product = new Product(); 152 | $product->name = 'My product'; 153 | $product->attributes = null; 154 | 155 | $manager = self::$kernel->getContainer()->get('doctrine')->getManagerForClass(Product::class); 156 | $manager->persist($product); 157 | $manager->flush(); 158 | $manager->clear(); 159 | 160 | $connection = $manager->getConnection(); 161 | $statement = $connection->executeQuery('SELECT * FROM Product'); 162 | $this->assertNull($statement->fetchAssociative()['attributes']); 163 | } 164 | 165 | public function testStoreAndRetrieveDocumentWithInstantiatedOtherSerializer(): void 166 | { 167 | /** 168 | * This call is necessary to cover this issue. 169 | * 170 | * @see https://github.com/dunglas/doctrine-json-odm/pull/78 171 | */ 172 | $serializer = self::$kernel->getContainer()->get('serializer'); 173 | 174 | $attribute1 = new Attribute(); 175 | $attribute1->key = 'foo'; 176 | $attribute1->value = 'bar'; 177 | 178 | $attribute2 = new Attribute(); 179 | $attribute2->key = 'weights'; 180 | $attribute2->value = [34, 67]; 181 | 182 | $attributes = [$attribute1, $attribute2]; 183 | 184 | $product = new Product(); 185 | $product->name = 'My product'; 186 | $product->attributes = $attributes; 187 | 188 | $manager = self::$kernel->getContainer()->get('doctrine')->getManagerForClass(Product::class); 189 | $manager->persist($product); 190 | $manager->flush(); 191 | 192 | $manager->clear(); 193 | 194 | $retrievedProduct = $manager->find(Product::class, $product->id); 195 | $this->assertEquals($attributes, $retrievedProduct->attributes); 196 | } 197 | 198 | /** 199 | * @requires PHP >= 8.1 200 | * @requires function \Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer::normalize 201 | */ 202 | public function testStoreAndRetrieveEnum(): void 203 | { 204 | $attribute = new Attribute(); 205 | $attribute->key = 'email'; 206 | $attribute->value = InputMode::EMAIL; 207 | 208 | $product = new Product(); 209 | $product->name = 'My product'; 210 | $product->attributes = [$attribute]; 211 | 212 | $manager = self::$kernel->getContainer()->get('doctrine')->getManagerForClass(Product::class); 213 | $manager->persist($product); 214 | $manager->flush(); 215 | 216 | $manager->clear(); 217 | 218 | $retrievedProduct = $manager->find(Product::class, $product->id); 219 | 220 | $this->assertSame(InputMode::EMAIL, $retrievedProduct->attributes[0]->value); 221 | } 222 | 223 | /** 224 | * @requires function \Symfony\Component\Serializer\Normalizer\UidNormalizer::normalize 225 | */ 226 | public function testStoreAndRetrieveUid(): void 227 | { 228 | $uuid = '1a87e1f2-1569-4493-a4a8-bc1915ca5631'; 229 | 230 | $attribute = new Attribute(); 231 | $attribute->key = 'uid'; 232 | $attribute->value = Uuid::fromString($uuid); 233 | 234 | $product = new Product(); 235 | $product->name = 'My product'; 236 | $product->attributes = [$attribute]; 237 | 238 | $manager = self::$kernel->getContainer()->get('doctrine')->getManagerForClass(Product::class); 239 | $manager->persist($product); 240 | $manager->flush(); 241 | 242 | $manager->clear(); 243 | 244 | $retrievedProduct = $manager->find(Product::class, $product->id); 245 | 246 | $this->assertInstanceOf(Uuid::class, $retrievedProduct->attributes[0]->value); 247 | $this->assertEquals($uuid, (string) $retrievedProduct->attributes[0]->value); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /tests/SerializerTest.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Dunglas\DoctrineJsonOdm\Tests; 11 | 12 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\AppKernelWithCustomTypeMapper; 13 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\AppKernelWithTypeMap; 14 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Attribute; 15 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Attributes; 16 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Bar; 17 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Baz; 18 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\ScalarValue; 19 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\WithMappedType; 20 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Entity\Foo; 21 | use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Enum\InputMode; 22 | use Symfony\Component\Uid\UuidV4; 23 | 24 | /** 25 | * @author Kévin Dunglas 26 | */ 27 | class SerializerTest extends AbstractKernelTestCase 28 | { 29 | public function testStoreAndRetrieveDocument(): void 30 | { 31 | $attribute1 = new Attribute(); 32 | $attribute1->key = 'foo'; 33 | $attribute1->value = 'bar'; 34 | 35 | $attribute2 = new Attribute(); 36 | $attribute2->key = 'weights'; 37 | $attribute2->value = [34, 67]; 38 | 39 | $attributes = [$attribute1, $attribute2]; 40 | 41 | $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); 42 | $data = $serializer->serialize($attributes, 'json'); 43 | $restoredAttributes = $serializer->deserialize($data, '', 'json'); 44 | 45 | $this->assertEquals($attributes, $restoredAttributes); 46 | } 47 | 48 | public function testStoreAndRetrieveDocumentsOfVariousTypes(): void 49 | { 50 | $bar = new Bar(); 51 | $bar->setTitle('Bar'); 52 | $bar->setWeight(12); 53 | 54 | $baz = new Baz(); 55 | $baz->setName('Baz'); 56 | $baz->setSize(7); 57 | 58 | $scalarValue = new ScalarValue('foobar'); 59 | 60 | $misc = [$bar, $baz, $scalarValue]; 61 | 62 | $foo = new Foo(); 63 | $foo->setName('Foo'); 64 | $foo->setMisc($misc); 65 | 66 | $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); 67 | $data = $serializer->serialize($misc, 'json'); 68 | $restoredMisc = $serializer->deserialize($data, '', 'json'); 69 | 70 | $this->assertEquals($misc, $restoredMisc); 71 | } 72 | 73 | public function testNestedObjects(): void 74 | { 75 | $attribute = new Attribute(); 76 | $attribute->key = 'nested'; 77 | $attribute->value = 'bar'; 78 | 79 | $attributeParent = new Attribute(); 80 | $attributeParent->key = 'parent'; 81 | $attributeParent->value = [[$attribute]]; 82 | 83 | $misc = [$attributeParent]; 84 | 85 | $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); 86 | $data = $serializer->serialize($misc, 'json'); 87 | $restoredMisc = $serializer->deserialize($data, '', 'json'); 88 | 89 | $this->assertEquals($misc, $restoredMisc); 90 | } 91 | 92 | public function testNestedObjectsInNestedObject(): void 93 | { 94 | $attribute1 = new Attribute(); 95 | $attribute1->key = 'attribute1'; 96 | 97 | $attribute2 = new Attribute(); 98 | $attribute2->key = 'attribute2'; 99 | 100 | $attributes = new Attributes(); 101 | $attributes->setAttributes([$attribute1, $attribute2]); 102 | 103 | $misc = [$attributes]; 104 | 105 | $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); 106 | $data = $serializer->serialize($misc, 'json'); 107 | $restoredMisc = $serializer->deserialize($data, '', 'json'); 108 | 109 | $this->assertEquals($misc, $restoredMisc); 110 | } 111 | 112 | public function testNullIsStoredAsNull(): void 113 | { 114 | $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); 115 | $data = $serializer->serialize(null, 'json'); 116 | $restoredMisc = $serializer->deserialize($data, '', 'json'); 117 | 118 | $this->assertEquals(null, $restoredMisc); 119 | } 120 | 121 | public function testScalarIsStoredInScalarKey(): void 122 | { 123 | $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); 124 | $value = new ScalarValue('foobar'); 125 | $data = $serializer->serialize($value, 'json'); 126 | $decodeData = json_decode($data, true); 127 | $this->assertArrayHasKey('#scalar', $decodeData); 128 | $this->assertSame($value->value(), $decodeData['#scalar']); 129 | $restoredValue = $serializer->deserialize($data, '', 'json'); 130 | 131 | $this->assertEquals($value, $restoredValue); 132 | } 133 | 134 | public function testTypeMappingIsOptional(): void 135 | { 136 | $this->assertFalse(self::$kernel->getContainer()->get('test.service_container')->has('dunglas_doctrine_json_odm.type_mapper')); 137 | 138 | $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); 139 | 140 | $value = new WithMappedType(); 141 | $data = $serializer->serialize($value, 'json'); 142 | $decodeData = json_decode($data, true); 143 | $this->assertArrayHasKey('#type', $decodeData); 144 | $this->assertSame(WithMappedType::class, $decodeData['#type']); 145 | $restoredValue = $serializer->deserialize($data, '', 'json'); 146 | 147 | $this->assertEquals($value, $restoredValue); 148 | } 149 | 150 | public function testTypeIsMappedFromConfig(): void 151 | { 152 | $this->useTypeMapConfig(); 153 | 154 | $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); 155 | 156 | $value = new WithMappedType(); 157 | $data = $serializer->serialize($value, 'json'); 158 | $decodeData = json_decode($data, true); 159 | $this->assertArrayHasKey('#type', $decodeData); 160 | $this->assertSame('mappedType', $decodeData['#type']); 161 | $restoredValue = $serializer->deserialize($data, '', 'json'); 162 | 163 | $this->assertEquals($value, $restoredValue); 164 | } 165 | 166 | public function testClassNameAlsoWorksForMappedTypes(): void 167 | { 168 | $this->useTypeMapConfig(); 169 | 170 | $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); 171 | 172 | $value = new WithMappedType(); 173 | $serialized = json_encode([ 174 | '#type' => WithMappedType::class, 175 | 'foo' => 'bar', 176 | ]); 177 | 178 | $restoredValue = $serializer->deserialize($serialized, '', 'json'); 179 | 180 | $this->assertEquals($value, $restoredValue); 181 | } 182 | 183 | public function testCustomTypeMapper(): void 184 | { 185 | self::ensureKernelShutdown(); 186 | self::$class = AppKernelWithCustomTypeMapper::class; 187 | self::bootKernel(); 188 | 189 | $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); 190 | 191 | $value = new WithMappedType(); 192 | $data = $serializer->serialize($value, 'json'); 193 | $decodeData = json_decode($data, true); 194 | $this->assertArrayHasKey('#type', $decodeData); 195 | $this->assertSame('customTypeAlias', $decodeData['#type']); 196 | $restoredValue = $serializer->deserialize($data, '', 'json'); 197 | 198 | $this->assertEquals($value, $restoredValue); 199 | } 200 | 201 | private function useTypeMapConfig(): void 202 | { 203 | self::ensureKernelShutdown(); 204 | self::$class = AppKernelWithTypeMap::class; 205 | self::bootKernel(); 206 | 207 | $this->assertTrue(self::$kernel->getContainer()->get('test.service_container')->has('dunglas_doctrine_json_odm.type_mapper')); 208 | } 209 | 210 | /** 211 | * @requires PHP >= 8.1 212 | * @requires function \Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer::normalize 213 | */ 214 | public function testSerializeEnum(): void 215 | { 216 | $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); 217 | 218 | $value = InputMode::EMAIL; 219 | $data = $serializer->serialize($value, 'json'); 220 | $decodeData = json_decode($data, true); 221 | $this->assertArrayHasKey('#type', $decodeData); 222 | $this->assertSame(InputMode::class, $decodeData['#type']); 223 | $restoredValue = $serializer->deserialize($data, '', 'json'); 224 | 225 | $this->assertEquals($value, $restoredValue); 226 | } 227 | 228 | /** 229 | * @requires function \Symfony\Component\Serializer\Normalizer\UidNormalizer::normalize 230 | */ 231 | public function testSerializeUid(): void 232 | { 233 | $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); 234 | 235 | $value = new UuidV4('4a7ba820-c806-4579-a907-193a2493863b'); 236 | $data = $serializer->serialize($value, 'json'); 237 | $decodeData = json_decode($data, true); 238 | $this->assertArrayHasKey('#type', $decodeData); 239 | $this->assertSame(UuidV4::class, $decodeData['#type']); 240 | $restoredValue = $serializer->deserialize($data, '', 'json'); 241 | 242 | $this->assertEquals($value, $restoredValue); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | use Symfony\Component\Console\Input\ArrayInput; 11 | use Symfony\Component\Console\Output\ConsoleOutput; 12 | use Symfony\Component\Console\Style\SymfonyStyle; 13 | 14 | date_default_timezone_set('UTC'); 15 | 16 | $loader = require __DIR__.'/../vendor/autoload.php'; 17 | require __DIR__.'/Fixtures/AppKernel.php'; 18 | 19 | if (!isset($_SERVER['DATABASE_URL'])) { 20 | (new SymfonyStyle(new ArrayInput([]), new ConsoleOutput()))->error("DATABASE_URL environment var is not set.\nCheck the CONTRIBUTING.md file for more details about how to test this project."); 21 | 22 | exit(1); 23 | } 24 | 25 | return $loader; 26 | --------------------------------------------------------------------------------