1212namespace Zenstruck \Foundry \ORM ;
1313
1414use Doctrine \ORM \EntityManagerInterface ;
15+ use Doctrine \ORM \Mapping \AssociationMapping ;
16+ use Doctrine \ORM \Mapping \ManyToOneAssociationMapping ;
1517use Doctrine \ORM \Mapping \MappingException as ORMMappingException ;
18+ use Doctrine \ORM \Mapping \OneToManyAssociationMapping ;
19+ use Doctrine \ORM \Mapping \OneToOneAssociationMapping ;
1620use Doctrine \Persistence \Mapping \MappingException ;
1721use Zenstruck \Foundry \Persistence \PersistenceStrategy ;
22+ use Zenstruck \Foundry \Persistence \Relationship \ManyToOneRelationship ;
23+ use Zenstruck \Foundry \Persistence \Relationship \OneToManyRelationship ;
24+ use Zenstruck \Foundry \Persistence \Relationship \OneToOneRelationship ;
25+ use Zenstruck \Foundry \Persistence \Relationship \RelationshipMetadata ;
1826
1927/**
2028 * @author Kevin Bond <kevinbond@gmail.com>
2432 * @method EntityManagerInterface objectManagerFor(string $class)
2533 * @method list<EntityManagerInterface> objectManagers()
2634 */
27- abstract class AbstractORMPersistenceStrategy extends PersistenceStrategy
35+ final class ORMPersistenceStrategy extends PersistenceStrategy
2836{
29- final public function contains (object $ object ): bool
37+ public function contains (object $ object ): bool
3038 {
3139 $ em = $ this ->objectManagerFor ($ object ::class);
3240
3341 return $ em ->contains ($ object ) && !$ em ->getUnitOfWork ()->isScheduledForInsert ($ object );
3442 }
3543
36- final public function hasChanges (object $ object ): bool
44+ public function hasChanges (object $ object ): bool
3745 {
3846 $ em = $ this ->objectManagerFor ($ object ::class);
3947
@@ -50,12 +58,12 @@ final public function hasChanges(object $object): bool
5058 return (bool ) $ unitOfWork ->getEntityChangeSet ($ object );
5159 }
5260
53- final public function truncate (string $ class ): void
61+ public function truncate (string $ class ): void
5462 {
5563 $ this ->objectManagerFor ($ class )->createQuery ("DELETE {$ class } e " )->execute ();
5664 }
5765
58- final public function embeddablePropertiesFor (object $ object , string $ owner ): ?array
66+ public function embeddablePropertiesFor (object $ object , string $ owner ): ?array
5967 {
6068 try {
6169 $ metadata = $ this ->objectManagerFor ($ owner )->getClassMetadata ($ object ::class);
@@ -76,17 +84,17 @@ final public function embeddablePropertiesFor(object $object, string $owner): ?a
7684 return $ properties ;
7785 }
7886
79- final public function isEmbeddable (object $ object ): bool
87+ public function isEmbeddable (object $ object ): bool
8088 {
8189 return $ this ->objectManagerFor ($ object ::class)->getClassMetadata ($ object ::class)->isEmbeddedClass ;
8290 }
8391
84- final public function isScheduledForInsert (object $ object ): bool
92+ public function isScheduledForInsert (object $ object ): bool
8593 {
8694 return $ this ->objectManagerFor ($ object ::class)->getUnitOfWork ()->isScheduledForInsert ($ object );
8795 }
8896
89- final public function managedNamespaces (): array
97+ public function managedNamespaces (): array
9098 {
9199 $ namespaces = [];
92100
@@ -97,7 +105,7 @@ final public function managedNamespaces(): array
97105 return \array_values (\array_merge (...$ namespaces ));
98106 }
99107
100- final public function getIdentifierValues (object $ object ): array
108+ public function getIdentifierValues (object $ object ): array
101109 {
102110 $ identifiers = $ this ->classMetadata ($ object ::class)->getIdentifierValues ($ object );
103111
@@ -118,4 +126,60 @@ function(mixed $value) use ($object) {
118126 $ identifiers
119127 );
120128 }
129+
130+ public function bidirectionalRelationshipMetadata (string $ parent , string $ child , string $ field ): ?RelationshipMetadata
131+ {
132+ $ associationMapping = $ this ->getAssociationMapping ($ parent , $ child , $ field );
133+
134+ if (null === $ associationMapping ) {
135+ return null ;
136+ }
137+
138+ if (!\is_a (
139+ $ child ,
140+ $ associationMapping ->targetEntity ,
141+ allow_string: true
142+ )) { // is_a() handles inheritance as well
143+ throw new \LogicException ("Cannot find correct association named \"{$ field }\" between classes [parent: \"{$ parent }\", child: \"{$ child }\"] " );
144+ }
145+
146+ $ inverseField = $ associationMapping ->isOwningSide () ? $ associationMapping ->inversedBy : $ associationMapping ->mappedBy ;
147+
148+ if (null === $ inverseField ) {
149+ return null ;
150+ }
151+
152+ return match (true ) {
153+ $ associationMapping instanceof OneToManyAssociationMapping => new OneToManyRelationship (
154+ inverseField: $ inverseField ,
155+ collectionIndexedBy: $ associationMapping ->isIndexed () ? $ associationMapping ->indexBy () : null
156+ ),
157+ $ associationMapping instanceof OneToOneAssociationMapping => new OneToOneRelationship (
158+ inverseField: $ inverseField ,
159+ isOwning: $ associationMapping ->isOwningSide ()
160+ ),
161+ $ associationMapping instanceof ManyToOneAssociationMapping => new ManyToOneRelationship (
162+ inverseField: $ inverseField ,
163+ ),
164+ default => null ,
165+ };
166+ }
167+
168+ /**
169+ * @param class-string $entityClass
170+ */
171+ private function getAssociationMapping (string $ entityClass , string $ targetEntity , string $ field ): ?AssociationMapping
172+ {
173+ try {
174+ $ associationMapping = $ this ->objectManagerFor ($ entityClass )->getClassMetadata ($ entityClass )->getAssociationMapping ($ field );
175+ } catch (MappingException |ORMMappingException ) {
176+ return null ;
177+ }
178+
179+ if (!\is_a ($ targetEntity , $ associationMapping ->targetEntity , allow_string: true )) {
180+ return null ;
181+ }
182+
183+ return $ associationMapping ;
184+ }
121185}
0 commit comments