├── .gitignore ├── ORM ├── NullableEmbeddable │ ├── ClosureNullator.php │ ├── EvaluatorInterface.php │ ├── NullatorInterface.php │ └── PropertyAccessor.php ├── NullableEmbeddableInterface.php ├── NullableEmbeddableListener.php └── NullableEmbeddableListenerFactory.php ├── README.md ├── Tests ├── EntityWithoutSetter.php ├── NullableEmbeddable.php └── NullableEmbeddable │ ├── ClosureNullatorTest.php │ └── NullableEmbeddableListenerTest.php ├── composer.json └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /ORM/NullableEmbeddable/ClosureNullator.php: -------------------------------------------------------------------------------- 1 | {$property} = null; 13 | }, $object, $object); 14 | 15 | $nullator($property); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ORM/NullableEmbeddable/EvaluatorInterface.php: -------------------------------------------------------------------------------- 1 | propertyAccessor = $propertyAccessor; 18 | } 19 | 20 | public static function createWithDefault() 21 | { 22 | return new self(new DefaultPropertyAccessor()); 23 | } 24 | 25 | public function isNull($object, $property): bool 26 | { 27 | $embeddable = $this->propertyAccessor->getValue($object, $property); 28 | if ($embeddable instanceof NullableEmbeddableInterface) { 29 | return $embeddable->isNull(); 30 | } 31 | 32 | return false; 33 | } 34 | 35 | public function setNull(&$object, $property) 36 | { 37 | $this->propertyAccessor->setValue($object, $property, null); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ORM/NullableEmbeddableInterface.php: -------------------------------------------------------------------------------- 1 | evaluator = $evaluator; 27 | $this->nullator = $nullator; 28 | } 29 | 30 | public function addMapping(string $entity, string $propertyPath) 31 | { 32 | if (empty($this->propertyMap[$entity])) { 33 | $this->propertyMap[$entity] = []; 34 | } 35 | 36 | $this->propertyMap[$entity][] = $propertyPath; 37 | } 38 | 39 | public function postLoad($object) 40 | { 41 | $entity = get_class($object); 42 | if (empty($this->propertyMap[$entity])) { 43 | return; 44 | } 45 | 46 | $entries = $this->propertyMap[$entity]; 47 | foreach ($entries as $property) { 48 | if ($this->evaluator->isNull($object, $property)) { 49 | $this->nullator->setNull($object, $property); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ORM/NullableEmbeddableListenerFactory.php: -------------------------------------------------------------------------------- 1 | addMapping('App\Domain\User\Model\UserProfile', 'address'); 35 | ``` 36 | 37 | 2. Now the listener needs to be registered with the Doctrine `EventManager`. 38 | ```php 39 | addMapping('App\Domain\User\Model\UserProfile', 'address'); 47 | 48 | $evm = new EventManager(); 49 | $evm->addEventListener([Events::postLoad], $listener); 50 | ``` 51 | 52 | **Tip:** It's highly recommended to use Doctrine [entity listener](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#entity-listeners) when configuring the listener, so it is only executed for the entities it actually applies to. 53 | 54 | ### Symfony 55 | 56 | In case you are using Symfony with Doctrine, you can register the listener as a service. 57 | 58 | ```yaml 59 | services: 60 | tarifhaus.doctrine.nullable_embeddable.property_accessor: 61 | public: false 62 | class: Tarifhaus\Doctrine\ORM\NullableEmbeddable\PropertyAccessor 63 | arguments: 64 | - '@property_accessor' 65 | 66 | tarifhaus.doctrine.nullable_embeddable_closure_nullator: 67 | public: false 68 | class: Tarifhaus\Doctrine\ORM\NullableEmbeddable\ClosureNullator 69 | 70 | tarifhaus.doctrine.nullable_embeddable_listener: 71 | public: false 72 | class: Tarifhaus\Doctrine\ORM\NullableEmbeddableListener 73 | arguments: 74 | - '@tarifhaus.doctrine.nullable_embeddable.property_accessor' 75 | - '@tarifhaus.doctrine.nullable_embeddable_closure_nullator' 76 | calls: 77 | - ['addMapping', ['App\Domain\User\Model\UserProfile', 'address']] 78 | tags: 79 | - { name: 'doctrine.orm.entity_listener', entity: '\App\Domain\User\Model\UserProfile', event: 'postLoad' } 80 | ``` 81 | -------------------------------------------------------------------------------- /Tests/EntityWithoutSetter.php: -------------------------------------------------------------------------------- 1 | property = $embeddable; 16 | } 17 | 18 | public function getProperty() 19 | { 20 | return $this->property; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/NullableEmbeddable.php: -------------------------------------------------------------------------------- 1 | isNull = $isNull; 16 | } 17 | 18 | public function isNull(): bool 19 | { 20 | return $this->isNull; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/NullableEmbeddable/ClosureNullatorTest.php: -------------------------------------------------------------------------------- 1 | listener = NullableEmbeddableListenerFactory::createWithClosureNullator(); 27 | } 28 | 29 | public function test_it_nullates_without_using_setter() 30 | { 31 | $embeddable = new NullableEmbeddable(true); 32 | $object = new EntityWithoutSetter($embeddable); 33 | 34 | $this->listener->addMapping(get_class($object), 'property'); 35 | $this->listener->postLoad($object); 36 | 37 | static::assertNull($object->getProperty()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/NullableEmbeddable/NullableEmbeddableListenerTest.php: -------------------------------------------------------------------------------- 1 | listener = NullableEmbeddableListenerFactory::createWithPropertyAccessor(); 27 | } 28 | 29 | public function test_it_replaces_nullable_embeddable_with_null() 30 | { 31 | $embeddable = new NullableEmbeddable(true); 32 | 33 | $object = new \stdClass(); 34 | $object->property = $embeddable; 35 | 36 | $this->listener->addMapping(get_class($object), 'property'); 37 | $this->listener->postLoad($object); 38 | 39 | static::assertNull($object->property); 40 | } 41 | 42 | public function test_it_does_not_replace_values() 43 | { 44 | $embeddable = new NullableEmbeddable(false); 45 | 46 | $object = new \stdClass(); 47 | $object->property = $embeddable; 48 | 49 | $this->listener->addMapping(get_class($object), 'property'); 50 | $this->listener->postLoad($object); 51 | 52 | static::assertSame($embeddable, $object->property); 53 | } 54 | 55 | public function test_it_does_not_alter_unmapped_properties() 56 | { 57 | $embeddable = new \stdClass(); 58 | 59 | $object = new \stdClass(); 60 | $object->property = $embeddable; 61 | 62 | $this->listener->postLoad($object); 63 | 64 | static::assertSame($embeddable, $object->property); 65 | } 66 | 67 | public function test_it_does_not_alter_unsupported_values() 68 | { 69 | $embeddable = new \stdClass(); 70 | 71 | $object = new \stdClass(); 72 | $object->property = $embeddable; 73 | 74 | $this->listener->addMapping(get_class($object), 'property'); 75 | $this->listener->postLoad($object); 76 | 77 | static::assertSame($embeddable, $object->property); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tarifhaus/doctrine-nullable-embeddable", 3 | "description": "A workaround implementation for nullable embeddables in Doctrine entities.", 4 | "license": "MIT", 5 | 6 | "autoload": { 7 | "psr-4": { "Tarifhaus\\Doctrine\\": "" }, 8 | "exclude-from-classmap": [ "/Tests/" ] 9 | }, 10 | 11 | "autoload-dev": { 12 | "psr-4": { "Tarifhaus\\Tests\\Doctrine\\ORM\\": "Tests" } 13 | }, 14 | 15 | "minimum-stability": "stable", 16 | "require": { 17 | "php": "^7.0", 18 | "symfony/property-access": "^3.2|^4.0" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^5.7" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | ./Tests 21 | 22 | 23 | 24 | 25 | 26 | . 27 | 28 | ./Tests 29 | ./* 30 | 31 | 32 | 33 | 34 | --------------------------------------------------------------------------------