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

Full embedded support #14

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
165 changes: 165 additions & 0 deletions EventListener/LoadORMEmbeddedMetadataSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<?php
/*
* This file is based on the code of the Sylius ResourceBundle.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
pink6440 marked this conversation as resolved.
Show resolved Hide resolved

namespace Joschi127\DoctrineEntityOverrideBundle\EventListener;

use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Symfony\Component\DependencyInjection\ContainerInterface;

class LoadORMEmbeddedMetadataSubscriber implements EventSubscriber {

/**
* @var ContainerInterface
*/
protected $container;

/**
* @var array
*/
protected $overriddenEntities;

/**
* @var array
*/
protected $parentClassesByClass = [];

/**
* Constructor
*
* @param array $overriddenEntities
*/
public function __construct(ContainerInterface $container, array $overriddenEntities)
{
$this->container = $container;
$this->overriddenEntities = $overriddenEntities;
foreach ($overriddenEntities as $interface => $class) {
$class = $this->getClass($class);
$this->parentClassesByClass[$class] = array_values(class_parents($class));
}
}

/**
* @return array
*/
public function getSubscribedEvents()
{
return array(
'loadClassMetadata'
);
}

/**
* @param LoadClassMetadataEventArgs $eventArgs
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();

// if (false === $eventArgs->getClassMetadata()->isEmbeddedClass)
// return ;

$metadata = $eventArgs->getClassMetadata() ;

foreach($metadata->embeddedClasses as $propertyName => $embeddedMapping) {

$overridingClass = $this->getOverridingClass($embeddedMapping['class']);

if (!$overridingClass)
continue ;

$targetMetadata = $eventArgs->getEntityManager()->getClassMetadata($overridingClass);

// Removing previous mappings

foreach($metadata->fieldMappings as $name => $mapping) {

if (!isset($mapping['declaredField']) || $mapping['declaredField']!=$propertyName)
continue;

unset($metadata->fieldMappings[$mapping['fieldName']]);
unset($metadata->columnNames[$mapping['fieldName']]);
unset($metadata->fieldNames[$mapping['columnName']]);
}

// Put the new mappings

$metadata->inlineEmbeddable($propertyName, $targetMetadata);
}


}




protected function getOverridingClass($className)
{
foreach ($this->overriddenEntities as $interface => $class) {
$interface = $this->getInterface($interface);
$class = $this->getClass($class);

if ($interface === $className) {
return $class;
}

foreach($this->parentClassesByClass[$class] as $parentClass) {
if ($parentClass === $className) {
return $class;
}
}
}

return null;
}

/**
* @param string $key
*
* @return string
* @throws \InvalidArgumentException
*/
protected function getInterface($key)
{
if ($this->container->hasParameter($key)) {
return $this->container->getParameter($key);
}

if (interface_exists($key) || class_exists($key)) {
return $key;
}

throw new \InvalidArgumentException(
sprintf('The interface or class %s does not exists.', $key)
);
}

/**
* @param string $key
*
* @return string
* @throws \InvalidArgumentException
*/
protected function getClass($key)
{
if ($this->container->hasParameter($key)) {
return $this->container->getParameter($key);
}

if (class_exists($key)) {
return $key;
}

throw new \InvalidArgumentException(
sprintf('The class %s does not exists.', $key)
);
}
}
16 changes: 16 additions & 0 deletions EventListener/LoadORMMetadataSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ protected function unsetFieldMappings(ClassMetadataInfo $metadata, $wasMappedSup
// if class is overridden and class is the interface itself, but was originally not defined as mapped
// superclass ... (class was set to isMappedSuperclass = true by this listener)
else if ($this->classIsOverridden($metadata->getName()) && !$wasMappedSuperclass) {
// to avoid :
// Property "..." in "<finalClass>" was already declared, but it must be declared only once
$metadata->embeddedClasses = [] ;

// ... set fields to declared / inherited to avoid MappingException "Duplicate definition of column" when
// loading the metadata of sub classes (this only happens if this class originally was defined as entity but
Expand All @@ -310,6 +313,15 @@ protected function unsetFieldMappings(ClassMetadataInfo $metadata, $wasMappedSup
// to make this work correctly, these fields will later be re-added in setFieldMappings() to the actual
// class (but we will keep the declared / inherited flags this time)
foreach ($metadata->fieldMappings as $name => $mapping) {

// fields are already declared
if ($this->isEmbeddedProperty($name, $mapping)) {
unset($metadata->fieldMappings[$mapping['fieldName']]);
unset($metadata->columnNames[$mapping['fieldName']]);
unset($metadata->fieldNames[$mapping['columnName']]);
continue;
}

if (!isset($mapping['declared'])) { // only if not already set
if (!$metadata->getReflectionClass()->getProperty($name)->isPrivate()) {
$metadata->fieldMappings[$mapping['fieldName']]['declared'] = $metadata->getName();
Expand Down Expand Up @@ -589,4 +601,8 @@ protected function getClass($key)
sprintf('The class %s does not exists.', $key)
);
}

public function isEmbeddedProperty($name, $mapping) {
return isset($mapping['declaredField']);
}
}
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,24 @@ Hints
original mapping configuration and only use your customized mapping.
* Have a look at the [Tests/Functional/src folder](https://github.com/joschi127/doctrine-entity-override-bundle/tree/master/Tests/Functional/src)
for some example code.

Embedded
-----
* rule sould be to initiate the embedded property in the constructor,
but, as we don't know that the embeddable class will be overriden,
it will be responsibility for the developper to use a kind of factory
to instantiate the default object property.
* that is not a problem when the entity is hydrated from database because
at this time, Doctrine will take care of it correctly.
```
/**
* @var Adresse
* @ORM\Embedded(class="adresse")
*/
protected $adresse;

public function __construct()
{
$this->adresse = new Adresse();
}
```
11 changes: 10 additions & 1 deletion Resources/config/services.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
parameters:
joschi127_doctrine_entity_override.event_subscriber.load_orm_metadata.class: Joschi127\DoctrineEntityOverrideBundle\EventListener\LoadORMMetadataSubscriber
joschi127_doctrine_entity_override.event_subscriber.load_embedded_orm_metadata.class: Joschi127\DoctrineEntityOverrideBundle\EventListener\LoadORMEmbeddedMetadataSubscriber

services:

Expand All @@ -9,4 +10,12 @@ services:
- "@service_container"
- "%joschi127_doctrine_entity_override.config.overridden_entities%"
tags:
- { name: doctrine.event_subscriber }
- { name: doctrine.event_subscriber, priority: 0 }

joschi127_doctrine_embedded_entity_override.event_subscriber.load_embedded_orm_metadata:
class: "%joschi127_doctrine_entity_override.event_subscriber.load_embedded_orm_metadata.class%"
arguments:
- "@service_container"
- "%joschi127_doctrine_entity_override.config.overridden_entities%"
tags:
- { name: doctrine.event_subscriber, priority: 2 }
8 changes: 8 additions & 0 deletions Tests/EntityOverride/CustomizedUserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Doctrine\ORM\EntityRepository;
use FOS\UserBundle\Doctrine\UserManager;
use Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity\CustomizedUser;
use Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity\EmbeddedAddressV2;
use Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity\Group;
use Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity\UserActivity;
use Joschi127\DoctrineEntityOverrideBundle\Tests\TestBase;
Expand Down Expand Up @@ -183,12 +184,19 @@ protected function doTestRepository($entityName)
'Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity\CustomizedUser',
$user
);

$cleanUser = $this->getNewTestUserObject();
$this->assertEquals($this->getTestUsername(), $user->getUsername());
$this->assertEquals($cleanUser->getFirstName(), $user->getFirstName());
$this->assertEquals($cleanUser->getLastName(), $user->getLastName());
$this->assertEquals($cleanUser->getEmail(), $user->getEmail());
$this->assertEquals($cleanUser->getPhoneNumber(), $user->getPhoneNumber());


$this->assertInstanceOf(
EmbeddedAddressV2::class,
$user->getAdresse()
);
}

protected function createUser()
Expand Down
1 change: 1 addition & 0 deletions Tests/Functional/app/config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ joschi127_doctrine_entity_override:
Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity\ExamplePlain: Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity\CustomizedExamplePlain
FOS\UserBundle\Model\User: Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity\CustomizedUser
Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity\AssociationExample: Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity\DemoNamespace\BetterAssociationExample
Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity\EmbeddedAdress: Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity\EmbeddedAdressV2

fos_user:
db_driver: orm
Expand Down
38 changes: 38 additions & 0 deletions Tests/Functional/src/Entity/EmbeddedAddress.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* Class EmbeddedAddress
* @package Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity
*
* @ORM\Embeddable()
*/
class EmbeddedAddress {
/**
* @var string|null
* @ORM\Column(type="string",nullable=true)
*/
protected $city;

/**
* @return string|null
*/
public function getCity()
{
return $this->city;
}

/**
* @param string|null $city
*/
public function setCity(?string $city)
{
$this->city = $city;
return $this;
}


}
40 changes: 40 additions & 0 deletions Tests/Functional/src/Entity/EmbeddedAddressV2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* Class EmbeddedAddressV2
* @package Joschi127\DoctrineEntityOverrideBundle\Tests\Functional\src\Entity
*
* @ORM\Embeddable()
*/
class EmbeddedAddressV2 extends EmbeddedAddress {
/**
* @var string|null
* @ORM\Column(type="string",nullable=true)
*/
protected $zip;

/**
* @return string|null
*/
public function getZip()
{
return $this->zip;
}

/**
* @param string|null $zip
*/
public function setZip(?string $zip)
{
$this->zip = $zip;
return $this;
}




}
Loading