-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7799 from doctrine/m2m-association-builder
Many to many association builder
- Loading branch information
Showing
19 changed files
with
1,192 additions
and
960 deletions.
There are no files selected for viewing
159 changes: 159 additions & 0 deletions
159
lib/Doctrine/ORM/Mapping/Builder/AssociationMetadataBuilder.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Doctrine\ORM\Mapping\Builder; | ||
|
||
use Doctrine\ORM\Annotation; | ||
use Doctrine\ORM\Mapping; | ||
use function array_diff; | ||
use function array_intersect; | ||
use function array_map; | ||
use function class_exists; | ||
use function constant; | ||
use function count; | ||
use function defined; | ||
use function in_array; | ||
use function interface_exists; | ||
use function sprintf; | ||
|
||
abstract class AssociationMetadataBuilder | ||
{ | ||
/** @var Mapping\ClassMetadataBuildingContext */ | ||
protected $metadataBuildingContext; | ||
|
||
/** @var CacheMetadataBuilder */ | ||
protected $cacheMetadataBuilder; | ||
|
||
/** @var Mapping\ClassMetadata */ | ||
protected $componentMetadata; | ||
|
||
/** @var string */ | ||
protected $fieldName; | ||
|
||
/** @var Annotation\Cache|null */ | ||
protected $cacheAnnotation; | ||
|
||
public function __construct( | ||
Mapping\ClassMetadataBuildingContext $metadataBuildingContext, | ||
?CacheMetadataBuilder $cacheMetadataBuilder = null | ||
) { | ||
$this->metadataBuildingContext = $metadataBuildingContext; | ||
$this->cacheMetadataBuilder = $cacheMetadataBuilder ?: new CacheMetadataBuilder($metadataBuildingContext); | ||
} | ||
|
||
public function withComponentMetadata(Mapping\ClassMetadata $componentMetadata) : AssociationMetadataBuilder | ||
{ | ||
$this->componentMetadata = $componentMetadata; | ||
|
||
$this->cacheMetadataBuilder->withComponentMetadata($componentMetadata); | ||
|
||
return $this; | ||
} | ||
|
||
public function withFieldName(string $fieldName) : AssociationMetadataBuilder | ||
{ | ||
$this->fieldName = $fieldName; | ||
|
||
$this->cacheMetadataBuilder->withFieldName($fieldName); | ||
|
||
return $this; | ||
} | ||
|
||
public function withCacheAnnotation(?Annotation\Cache $cacheAnnotation) : AssociationMetadataBuilder | ||
{ | ||
$this->cacheAnnotation = $cacheAnnotation; | ||
|
||
if ($cacheAnnotation !== null) { | ||
$this->cacheMetadataBuilder->withCacheAnnotation($cacheAnnotation); | ||
} | ||
|
||
return $this; | ||
} | ||
|
||
protected function buildCache(Mapping\AssociationMetadata $associationMetadata) : void | ||
{ | ||
if ($this->cacheAnnotation !== null) { | ||
$associationMetadata->setCache($this->cacheMetadataBuilder->build()); | ||
} | ||
} | ||
|
||
/** | ||
* Attempts to resolve target entity. | ||
* | ||
* @param string $targetEntity The proposed target entity | ||
* | ||
* @return string The processed target entity | ||
* | ||
* @throws Mapping\MappingException If a target entity is not valid. | ||
*/ | ||
protected function getTargetEntity(string $targetEntity) : string | ||
{ | ||
// Validate if target entity is defined | ||
if (! $targetEntity) { | ||
throw Mapping\MappingException::missingTargetEntity($this->fieldName); | ||
} | ||
|
||
// Validate that target entity exists | ||
if (! (class_exists($targetEntity) || interface_exists($targetEntity))) { | ||
throw Mapping\MappingException::invalidTargetEntityClass( | ||
$targetEntity, | ||
$this->componentMetadata->getClassName(), | ||
$this->fieldName | ||
); | ||
} | ||
|
||
return $targetEntity; | ||
} | ||
|
||
/** | ||
* Attempts to resolve the cascade modes. | ||
* | ||
* @param string[] $originalCascades The original unprocessed field cascades. | ||
* | ||
* @return string[] The processed field cascades. | ||
* | ||
* @throws Mapping\MappingException If a cascade option is not valid. | ||
*/ | ||
protected function getCascade(array $originalCascades) : array | ||
{ | ||
$cascadeTypes = ['remove', 'persist', 'refresh']; | ||
$cascades = array_map('strtolower', $originalCascades); | ||
|
||
if (in_array('all', $cascades, true)) { | ||
$cascades = $cascadeTypes; | ||
} | ||
|
||
if (count($cascades) !== count(array_intersect($cascades, $cascadeTypes))) { | ||
$diffCascades = array_diff($cascades, array_intersect($cascades, $cascadeTypes)); | ||
|
||
throw Mapping\MappingException::invalidCascadeOption( | ||
$diffCascades, | ||
$this->componentMetadata->getClassName(), | ||
$this->fieldName | ||
); | ||
} | ||
|
||
return $cascades; | ||
} | ||
|
||
/** | ||
* Attempts to resolve the fetch mode. | ||
* | ||
* @param string $fetchMode The fetch mode. | ||
* | ||
* @return string The fetch mode as defined in ClassMetadata. | ||
* | ||
* @throws Mapping\MappingException If the fetch mode is not valid. | ||
*/ | ||
protected function getFetchMode($fetchMode) : string | ||
{ | ||
$fetchModeConstant = sprintf('%s::%s', Mapping\FetchMode::class, $fetchMode); | ||
|
||
if (! defined($fetchModeConstant)) { | ||
throw Mapping\MappingException::invalidFetchMode($this->componentMetadata->getClassName(), $fetchMode); | ||
} | ||
|
||
return constant($fetchModeConstant); | ||
} | ||
} |
131 changes: 131 additions & 0 deletions
131
lib/Doctrine/ORM/Mapping/Builder/ManyToManyAssociationMetadataBuilder.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Doctrine\ORM\Mapping\Builder; | ||
|
||
use Doctrine\ORM\Annotation; | ||
use Doctrine\ORM\Mapping; | ||
use function array_merge; | ||
use function array_unique; | ||
use function assert; | ||
|
||
class ManyToManyAssociationMetadataBuilder extends ToManyAssociationMetadataBuilder | ||
{ | ||
/** @var JoinTableMetadataBuilder */ | ||
private $joinTableMetadataBuilder; | ||
|
||
/** @var Annotation\ManyToMany */ | ||
private $manyToManyAnnotation; | ||
|
||
/** @var Annotation\JoinTable|null */ | ||
private $joinTableAnnotation; | ||
|
||
public function __construct( | ||
Mapping\ClassMetadataBuildingContext $metadataBuildingContext, | ||
?JoinTableMetadataBuilder $joinTableMetadataBuilder = null, | ||
?CacheMetadataBuilder $cacheMetadataBuilder = null | ||
) { | ||
parent::__construct($metadataBuildingContext, $cacheMetadataBuilder); | ||
|
||
$this->joinTableMetadataBuilder = $joinTableMetadataBuilder ?: new JoinTableMetadataBuilder($metadataBuildingContext); | ||
} | ||
|
||
public function withComponentMetadata(Mapping\ClassMetadata $componentMetadata) : AssociationMetadataBuilder | ||
{ | ||
parent::withComponentMetadata($componentMetadata); | ||
|
||
$this->joinTableMetadataBuilder->withComponentMetadata($componentMetadata); | ||
|
||
return $this; | ||
} | ||
|
||
public function withFieldName(string $fieldName) : AssociationMetadataBuilder | ||
{ | ||
parent::withFieldName($fieldName); | ||
|
||
$this->joinTableMetadataBuilder->withFieldName($fieldName); | ||
|
||
return $this; | ||
} | ||
|
||
public function withManyToManyAnnotation(Annotation\ManyToMany $manyToManyAnnotation) : ManyToManyAssociationMetadataBuilder | ||
{ | ||
$this->manyToManyAnnotation = $manyToManyAnnotation; | ||
|
||
return $this; | ||
} | ||
|
||
public function withJoinTableAnnotation(?Annotation\JoinTable $joinTableAnnotation) : ManyToManyAssociationMetadataBuilder | ||
{ | ||
$this->joinTableAnnotation = $joinTableAnnotation; | ||
|
||
if ($joinTableAnnotation !== null) { | ||
$this->joinTableMetadataBuilder->withJoinTableAnnotation($joinTableAnnotation); | ||
} | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* @internal Association metadata order of definition settings is important. | ||
*/ | ||
public function build() : Mapping\ManyToManyAssociationMetadata | ||
{ | ||
// Validate required fields | ||
assert($this->componentMetadata !== null); | ||
assert($this->manyToManyAnnotation !== null); | ||
assert($this->fieldName !== null); | ||
|
||
$componentClassName = $this->componentMetadata->getClassName(); | ||
$associationMetadata = new Mapping\ManyToManyAssociationMetadata($this->fieldName); | ||
|
||
$associationMetadata->setSourceEntity($componentClassName); | ||
$associationMetadata->setTargetEntity($this->getTargetEntity($this->manyToManyAnnotation->targetEntity)); | ||
$associationMetadata->setCascade($this->getCascade($this->manyToManyAnnotation->cascade)); | ||
$associationMetadata->setFetchMode($this->getFetchMode($this->manyToManyAnnotation->fetch)); | ||
$associationMetadata->setOwningSide(true); | ||
|
||
if (! empty($this->manyToManyAnnotation->mappedBy)) { | ||
$associationMetadata->setMappedBy($this->manyToManyAnnotation->mappedBy); | ||
$associationMetadata->setOwningSide(false); | ||
} | ||
|
||
if (! empty($this->manyToManyAnnotation->inversedBy)) { | ||
$associationMetadata->setInversedBy($this->manyToManyAnnotation->inversedBy); | ||
} | ||
|
||
if (! empty($this->manyToManyAnnotation->indexBy)) { | ||
$associationMetadata->setIndexedBy($this->manyToManyAnnotation->indexBy); | ||
} | ||
|
||
if ($this->manyToManyAnnotation->orphanRemoval) { | ||
$associationMetadata->setOrphanRemoval($this->manyToManyAnnotation->orphanRemoval); | ||
|
||
// Orphan removal also implies a cascade remove | ||
$associationMetadata->setCascade(array_unique(array_merge($associationMetadata->getCascade(), ['remove']))); | ||
} | ||
|
||
if ($this->orderByAnnotation !== null) { | ||
$associationMetadata->setOrderBy($this->orderByAnnotation->value); | ||
} | ||
|
||
$this->buildCache($associationMetadata); | ||
|
||
// Check for owning side to consider join column | ||
if (! $associationMetadata->isOwningSide()) { | ||
return $associationMetadata; | ||
} | ||
|
||
$this->buildJoinTable($associationMetadata); | ||
|
||
return $associationMetadata; | ||
} | ||
|
||
protected function buildJoinTable(Mapping\ManyToManyAssociationMetadata $associationMetadata) : void | ||
{ | ||
$this->joinTableMetadataBuilder->withTargetEntity($associationMetadata->getTargetEntity()); | ||
|
||
$associationMetadata->setJoinTable($this->joinTableMetadataBuilder->build()); | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
lib/Doctrine/ORM/Mapping/Builder/ManyToOneAssociationMetadataBuilder.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Doctrine\ORM\Mapping\Builder; | ||
|
||
use Doctrine\ORM\Annotation; | ||
use Doctrine\ORM\Mapping; | ||
use function assert; | ||
|
||
class ManyToOneAssociationMetadataBuilder extends ToOneAssociationMetadataBuilder | ||
{ | ||
/** @var Annotation\ManyToOne */ | ||
private $manyToOneAnnotation; | ||
|
||
public function withManyToOneAnnotation(Annotation\ManyToOne $manyToOneAnnotation) : ManyToOneAssociationMetadataBuilder | ||
{ | ||
$this->manyToOneAnnotation = $manyToOneAnnotation; | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* @internal Association metadata order of definition settings is important. | ||
*/ | ||
public function build() : Mapping\ManyToOneAssociationMetadata | ||
{ | ||
// Validate required fields | ||
assert($this->componentMetadata !== null); | ||
assert($this->manyToOneAnnotation !== null); | ||
assert($this->fieldName !== null); | ||
|
||
$componentClassName = $this->componentMetadata->getClassName(); | ||
$associationMetadata = new Mapping\ManyToOneAssociationMetadata($this->fieldName); | ||
|
||
$associationMetadata->setSourceEntity($componentClassName); | ||
$associationMetadata->setTargetEntity($this->getTargetEntity($this->manyToOneAnnotation->targetEntity)); | ||
$associationMetadata->setCascade($this->getCascade($this->manyToOneAnnotation->cascade)); | ||
$associationMetadata->setFetchMode($this->getFetchMode($this->manyToOneAnnotation->fetch)); | ||
$associationMetadata->setOwningSide(true); | ||
|
||
if (! empty($this->manyToOneAnnotation->inversedBy)) { | ||
$associationMetadata->setInversedBy($this->manyToOneAnnotation->inversedBy); | ||
} | ||
|
||
$this->buildCache($associationMetadata); | ||
$this->buildPrimaryKey($associationMetadata); | ||
$this->buildJoinColumns($associationMetadata); | ||
|
||
return $associationMetadata; | ||
} | ||
} |
Oops, something went wrong.