Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow using type names in place of class names #118

Merged
merged 25 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9d6e5e2
Working prototype
TamasSzigeti Sep 1, 2022
751719a
Style fixes
TamasSzigeti Sep 1, 2022
3514c64
Add arg type to config validator callback
TamasSzigeti Oct 17, 2022
ed629aa
Inject type mapper as optional service
TamasSzigeti Oct 17, 2022
343bcc0
Add tests
TamasSzigeti Oct 17, 2022
b338d56
Test without type mapper service
TamasSzigeti Oct 17, 2022
a299103
With and without type mapper
TamasSzigeti Oct 17, 2022
488b3d5
Assert service exists vs not
TamasSzigeti Oct 17, 2022
22f2ce8
Apply linter diff
TamasSzigeti Oct 17, 2022
144fcc9
Add doc
TamasSzigeti Oct 17, 2022
35c2633
Rename config key to type_map
TamasSzigeti Oct 17, 2022
91caa7b
Define type mapper service in xml
TamasSzigeti Oct 17, 2022
bb1ac96
Make type mapper final
TamasSzigeti Oct 17, 2022
ea36396
Do not use deprecated static test container
TamasSzigeti Oct 17, 2022
b22e8fa
Use forward compatible test container access
TamasSzigeti Oct 17, 2022
49bb617
Fix condition for removing enum normalizer
TamasSzigeti Oct 17, 2022
672c7a9
Address deprecation error
TamasSzigeti Oct 17, 2022
0fba0e3
lint
TamasSzigeti Oct 17, 2022
d01cf41
Add test for custom type mapper
TamasSzigeti Oct 17, 2022
2721cc2
lint
TamasSzigeti Oct 17, 2022
1d74beb
test compatibility w earlier versions
TamasSzigeti Oct 18, 2022
fdf4cc6
Use legacy password hashing in MySQL for old php versions
TamasSzigeti Oct 18, 2022
333a2f3
Add doc, drop symfony param, resolve wrong var naming
TamasSzigeti Feb 10, 2023
469d843
doc fixes
TamasSzigeti Feb 18, 2023
558fa73
Fix mysql in workflow, run on 8.2, no fail-fast
TamasSzigeti Feb 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,34 @@ $foo = $entityManager->find(Foo::class, $foo->getId());
var_dump($foo->misc); // Same as what we set earlier
```

### Using type aliases

Using custom type aliases as `#type` rather than FQCNs has a couple of benefits:
- In case you move or rename your document classes, you can just update your type map without migrating db content
TamasSzigeti marked this conversation as resolved.
Show resolved Hide resolved
- For applications that might store millions of records with json documents, this can also save some storage space
TamasSzigeti marked this conversation as resolved.
Show resolved Hide resolved

In order to use type aliases, add the bundle configuration, e.g. in `config/packages/doctrine_json_odm.yaml`:
TamasSzigeti marked this conversation as resolved.
Show resolved Hide resolved
```yaml
dunglas_doctrine_json_odm:
type_map:
foo: App\Something\Foo
bar: App\SomethingElse\Bar
```

With this, `Foo` objects will be serialized as
TamasSzigeti marked this conversation as resolved.
Show resolved Hide resolved
```json
{ "#type": "foo", "someProperty": "someValue" }
```

Another option is to use your own custom type mapper implementing `Dunglas\DoctrineJsonOdm\TypeMapperInterface`. For this, just override the service definition:

```yaml
dunglas marked this conversation as resolved.
Show resolved Hide resolved
services:
dunglas_doctrine_json_odm.type_mapper: '@App\Something\MyFancyTypeMapper'
```

You can add type aliases at any point in time. Already persisted json documents with class names will still get deserialized correctly.

### Limitations when updating nested properties

Due to how Doctrine works, it will not detect changes to nested objects or properties.
Expand Down Expand Up @@ -247,7 +275,7 @@ As a side note: If you happen to use [Autowiring](https://symfony.com/doc/curren

**When the namespace of a used entity changes**

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.
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.

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.

Expand Down
43 changes: 43 additions & 0 deletions src/Bundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Dunglas\DoctrineJsonOdm\Bundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
TamasSzigeti marked this conversation as resolved.
Show resolved Hide resolved
{
/**
* @return TreeBuilder
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('dunglas_doctrine_json_odm');

$treeBuilder->getRootNode()
->children()
->arrayNode('type_map')
->defaultValue([])
->useAttributeAsKey('type')
->scalarPrototype()
->cannotBeEmpty()
->validate()
->ifTrue(static function (string $v): bool {
return !class_exists($v);
})
->thenInvalid('Use fully qualified classnames as type values')
->end()
->end()
->end()
->end();

return $treeBuilder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,17 @@ public function load(array $configs, ContainerBuilder $container): void
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.xml');

if (!class_exists(BackedEnumNormalizer::class) || !class_exists(\BackedEnum::class)) {
if (PHP_VERSION_ID < 80100 || !class_exists(BackedEnumNormalizer::class)) {
$container->removeDefinition('dunglas_doctrine_json_odm.normalizer.backed_enum');
}

$config = $this->processConfiguration(new Configuration(), $configs);

$typeMapConfig = $config['type_map'] ?? [];
$container->setParameter('dunglas_doctrine_json_odm.type_map', $typeMapConfig);

if (!$typeMapConfig) {
$container->removeDefinition('dunglas_doctrine_json_odm.type_mapper');
}
TamasSzigeti marked this conversation as resolved.
Show resolved Hide resolved
}
}
6 changes: 6 additions & 0 deletions src/Bundle/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

<service id="dunglas_doctrine_json_odm.normalizer.backed_enum" class="Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer" public="false" />

<service id="dunglas_doctrine_json_odm.type_mapper" class="Dunglas\DoctrineJsonOdm\TypeMapper" public="false">
<argument>%dunglas_doctrine_json_odm.type_map%</argument>
TamasSzigeti marked this conversation as resolved.
Show resolved Hide resolved
</service>

<service id="dunglas_doctrine_json_odm.serializer" class="Dunglas\DoctrineJsonOdm\Serializer" public="true">
<argument type="collection">
<argument type="service" id="dunglas_doctrine_json_odm.normalizer.backed_enum" on-invalid="ignore" />
Expand All @@ -27,6 +31,8 @@
<argument type="collection">
<argument type="service" id="serializer.encoder.json" />
</argument>

<argument type="service" id="dunglas_doctrine_json_odm.type_mapper" on-invalid="ignore" />
</service>
</services>
</container>
34 changes: 33 additions & 1 deletion src/SerializerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,34 @@

namespace Dunglas\DoctrineJsonOdm;

use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
* @internal
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
trait SerializerTrait
{
/**
* @var TypeMapperInterface|null
*/
private $typeMapper;

/**
* @param (NormalizerInterface|DenormalizerInterface)[] $normalizers
* @param (EncoderInterface|DecoderInterface)[] $encoders
*/
public function __construct(array $normalizers = [], array $encoders = [], ?TypeMapperInterface $typeMapper = null)
{
parent::__construct($normalizers, $encoders);

$this->typeMapper = $typeMapper;
}

/**
* @param mixed $data
* @param string|null $format
Expand All @@ -27,7 +48,13 @@ public function normalize($data, $format = null, array $context = [])
$normalizedData = parent::normalize($data, $format, $context);

if (\is_object($data)) {
$typeData = [self::KEY_TYPE => \get_class($data)];
$typeName = \get_class($data);

if ($this->typeMapper) {
$typeName = $this->typeMapper->getTypeByClass($typeName);
}

$typeData = [self::KEY_TYPE => $typeName];
$valueData = is_scalar($normalizedData) ? [self::KEY_SCALAR => $normalizedData] : $normalizedData;
$normalizedData = array_merge($typeData, $valueData);
}
Expand All @@ -44,6 +71,11 @@ public function denormalize($data, $type, $format = null, array $context = [])
{
if (\is_array($data) && (isset($data[self::KEY_TYPE]))) {
$keyType = $data[self::KEY_TYPE];

if ($this->typeMapper) {
$keyType = $this->typeMapper->getClassByType($keyType);
}

unset($data[self::KEY_TYPE]);

$data = $data[self::KEY_SCALAR] ?? $data;
Expand Down
55 changes: 55 additions & 0 deletions src/TypeMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

/*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Dunglas\DoctrineJsonOdm;

/**
* Allows using string constants in place of class names.
*/
final class TypeMapper implements TypeMapperInterface
{
/**
* @var array<class-string, string>
*/
private $classToType;

/**
* @var array<string, class-string>
*/
private $typeToClass;

/**
* @param array<class-string, string> $classToType
*/
public function __construct(array $classToType)
{
$this->classToType = $classToType;
$this->typeToClass = array_flip($classToType);
}

/**
* Falls back to class name itself.
*
* @param class-string $class
*/
public function getTypeByClass(string $class): string
{
return $this->typeToClass[$class] ?? $class;
}

/**
* Falls back to type name itself – it might as well be a class.
*
* @return class-string
*/
public function getClassByType(string $type): string
{
return $this->classToType[$type] ?? $type;
}
}
30 changes: 30 additions & 0 deletions src/TypeMapperInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Dunglas\DoctrineJsonOdm;

/**
* Allows using string constants in place of class names.
*/
interface TypeMapperInterface
{
/**
* Resolve type name from class.
*
* @param class-string $class
*/
public function getTypeByClass(string $class): string;

/**
* Resolve class from type name.
*
* @return class-string
*/
public function getClassByType(string $type): string;
}
1 change: 0 additions & 1 deletion tests/Fixtures/AppKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ protected function configureContainer(ContainerBuilder $container, LoaderInterfa
'test' => null,
]);

$db = getenv('DB');
$container->loadFromExtension('doctrine', [
'dbal' => [
'url' => '%env(resolve:DATABASE_URL)%',
Expand Down
26 changes: 26 additions & 0 deletions tests/Fixtures/AppKernelWithCustomTypeMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures;

use AppKernel;
use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\CustomTypeMapper;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;

class AppKernelWithCustomTypeMapper extends AppKernel
{
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
{
parent::configureContainer($container, $loader);

$container->setDefinition('dunglas_doctrine_json_odm.type_mapper', new Definition(CustomTypeMapper::class));
}
}
29 changes: 29 additions & 0 deletions tests/Fixtures/AppKernelWithTypeMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures;

use AppKernel;
use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\WithMappedType;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AppKernelWithTypeMap extends AppKernel
{
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
{
parent::configureContainer($container, $loader);

$container->loadFromExtension('dunglas_doctrine_json_odm', [
'type_map' => [
'mappedType' => WithMappedType::class,
],
]);
}
}
26 changes: 26 additions & 0 deletions tests/Fixtures/TestBundle/CustomTypeMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle;

use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\WithMappedType;
use Dunglas\DoctrineJsonOdm\TypeMapperInterface;

class CustomTypeMapper implements TypeMapperInterface
{
public function getTypeByClass(string $class): string
{
return 'customTypeAlias';
}

public function getClassByType(string $type): string
{
return WithMappedType::class;
}
}
15 changes: 15 additions & 0 deletions tests/Fixtures/TestBundle/Document/WithMappedType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

/*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document;

class WithMappedType
{
public $foo = 'bar';
}
Loading