Skip to content

Commit f539cf8

Browse files
authored
Add LookupStrategy to DoctrineWriter (port of #11 for 2.x) (#31)
Additive 2.x port of lookup strategies with tests; BC defaults preserved.
1 parent 31fd345 commit f539cf8

4 files changed

Lines changed: 481 additions & 106 deletions

File tree

src/DoctrineWriter.php

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Doctrine\Persistence\ObjectManager;
1111
use Doctrine\Persistence\ObjectRepository;
1212
use Port\Doctrine\Exception\UnsupportedDatabaseTypeException;
13+
use Port\Doctrine\LookupStrategy\FieldsLookupStrategy;
1314
use Port\Writer;
1415

1516
/**
@@ -72,25 +73,46 @@ class DoctrineWriter implements Writer, Writer\FlushableWriter
7273
/**
7374
* Method used for looking up the item
7475
*
75-
* @var array
76+
* @var array|callable
7677
*/
7778
protected $lookupMethod;
7879

80+
/**
81+
* Strategy used to look up existing entities when truncate is disabled.
82+
*/
83+
private LookupStrategy $lookupStrategy;
84+
7985
private Inflector $inflector;
8086

87+
/**
88+
* Create a Doctrine writer with a custom object lookup strategy.
89+
*
90+
* Prefer this when you need QueryBuilder-based or other non-field lookups
91+
* (see https://github.com/portphp/doctrine/issues/3).
92+
*/
93+
public static function withLookupStrategy(
94+
ObjectManager $objectManager,
95+
string $objectName,
96+
LookupStrategy $lookupStrategy
97+
): self {
98+
return new self($objectManager, $objectName, null, 'findOneBy', $lookupStrategy);
99+
}
100+
81101
/**
82102
* Constructor
83103
*
84-
* @param ObjectManager $objectManager
85-
* @param string $objectName
86-
* @param string|array $index Field or fields to find current entities by
87-
* @param string $lookupMethod Method used for looking up the item
104+
* @param ObjectManager $objectManager
105+
* @param string $objectName
106+
* @param string|array|null $index Field or fields to find current entities by
107+
* @param string $lookupMethod Method used for looking up the item
108+
* @param LookupStrategy|null $lookupStrategy Optional custom strategy (overrides $index / $lookupMethod)
88109
*/
89110
public function __construct(
90111
ObjectManager $objectManager,
91112
$objectName,
92113
$index = null,
93-
$lookupMethod = 'findOneBy'
114+
$lookupMethod = 'findOneBy',
115+
?LookupStrategy $lookupStrategy = null
94116
) {
95117
$this->ensureSupportedObjectManager($objectManager);
96118
$this->objectManager = $objectManager;
@@ -116,6 +138,11 @@ public function __construct(
116138
);
117139
}
118140
$this->lookupMethod = [$this->objectRepository, $lookupMethod];
141+
$this->lookupStrategy = $lookupStrategy ?? new FieldsLookupStrategy(
142+
$this->objectRepository,
143+
$index,
144+
$lookupMethod
145+
);
119146
$this->inflector = InflectorFactory::create()->build();
120147
}
121148

@@ -308,27 +335,15 @@ protected function reEnableLogging()
308335

309336
protected function findOrCreateItem(array $item): object
310337
{
311-
$object = null;
312-
// If the table was not truncated to begin with, find current object
313-
// first
338+
// If the table was not truncated to begin with, find current object first
314339
if (!$this->truncate) {
315-
if (!empty($this->lookupFields)) {
316-
$lookupConditions = [];
317-
foreach ($this->lookupFields as $fieldName) {
318-
$lookupConditions[$fieldName] = $item[$fieldName];
319-
}
320-
321-
$object = call_user_func($this->lookupMethod, $lookupConditions);
322-
} else {
323-
$object = $this->objectRepository->find(current($item));
340+
$object = $this->lookupStrategy->lookup($item);
341+
if ($object !== null) {
342+
return $object;
324343
}
325344
}
326345

327-
if (!$object) {
328-
return $this->getNewInstance();
329-
}
330-
331-
return $object;
346+
return $this->getNewInstance();
332347
}
333348

334349
protected function ensureSupportedObjectManager(ObjectManager $objectManager)

src/LookupStrategy.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Port\Doctrine;
4+
5+
/**
6+
* Finds existing objects in the database for upsert-style writes.
7+
*
8+
* Implement this interface to customize how {@see DoctrineWriter} looks up
9+
* entities (e.g. via QueryBuilder or custom repository methods).
10+
*/
11+
interface LookupStrategy
12+
{
13+
/**
14+
* Look up an existing object for the given import item.
15+
*
16+
* @param array $item Import item (field => value)
17+
*
18+
* @return object|null Null if no object was found (writer will create a new instance)
19+
*/
20+
public function lookup(array $item): ?object;
21+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
namespace Port\Doctrine\LookupStrategy;
4+
5+
use Doctrine\Persistence\ObjectManager;
6+
use Doctrine\Persistence\ObjectRepository;
7+
use Port\Doctrine\LookupStrategy;
8+
9+
/**
10+
* Default lookup strategy using repository field criteria.
11+
*
12+
* Behavior matches historical DoctrineWriter lookup:
13+
* - With lookup fields: call the repository method (default findOneBy) with those criteria
14+
* - Without lookup fields: ObjectRepository::find(current($item))
15+
*/
16+
class FieldsLookupStrategy implements LookupStrategy
17+
{
18+
private ObjectRepository $objectRepository;
19+
20+
/** @var list<string> */
21+
private array $lookupFields;
22+
23+
private string $lookupMethod;
24+
25+
/**
26+
* @param ObjectRepository $objectRepository
27+
* @param string|array|null $index Field or fields used as lookup criteria (null = find by first item value)
28+
* @param string $lookupMethod Repository method used when lookup fields are set
29+
*/
30+
public function __construct(
31+
ObjectRepository $objectRepository,
32+
string|array|null $index = null,
33+
string $lookupMethod = 'findOneBy'
34+
) {
35+
$this->objectRepository = $objectRepository;
36+
$this->lookupFields = $this->normalizeIndex($index);
37+
$this->assertLookupMethod($lookupMethod);
38+
$this->lookupMethod = $lookupMethod;
39+
}
40+
41+
/**
42+
* Convenience factory from an object manager and class name.
43+
*/
44+
public static function fromObjectManager(
45+
ObjectManager $objectManager,
46+
string $objectName,
47+
string|array|null $index = null,
48+
string $lookupMethod = 'findOneBy'
49+
): self {
50+
return new self(
51+
$objectManager->getRepository($objectName),
52+
$index,
53+
$lookupMethod
54+
);
55+
}
56+
57+
/**
58+
* @param string $field Field to find current objects by
59+
*/
60+
public function withLookupField(string $field): self
61+
{
62+
return $this->withLookupFields([$field]);
63+
}
64+
65+
/**
66+
* @param list<string> $fields Fields to find current objects by
67+
*/
68+
public function withLookupFields(array $fields): self
69+
{
70+
$new = clone $this;
71+
$new->lookupFields = array_values($fields);
72+
73+
return $new;
74+
}
75+
76+
/**
77+
* Accept string or list of fields (mirrors DoctrineWriter $index constructor arg).
78+
*
79+
* @param string|array $index
80+
*/
81+
public function withIndex(string|array $index): self
82+
{
83+
if (is_array($index)) {
84+
return $this->withLookupFields($index);
85+
}
86+
87+
return $this->withLookupField($index);
88+
}
89+
90+
/**
91+
* Doctrine repository method for finding objects when lookup fields are set.
92+
*/
93+
public function withLookupMethod(string $lookupMethod): self
94+
{
95+
$this->assertLookupMethod($lookupMethod);
96+
97+
$new = clone $this;
98+
$new->lookupMethod = $lookupMethod;
99+
100+
return $new;
101+
}
102+
103+
public function lookup(array $item): ?object
104+
{
105+
if (!empty($this->lookupFields)) {
106+
$lookupConditions = [];
107+
foreach ($this->lookupFields as $fieldName) {
108+
$lookupConditions[$fieldName] = $item[$fieldName] ?? null;
109+
}
110+
111+
$result = $this->objectRepository->{$this->lookupMethod}($lookupConditions);
112+
113+
return is_object($result) ? $result : null;
114+
}
115+
116+
$result = $this->objectRepository->find(current($item));
117+
118+
return is_object($result) ? $result : null;
119+
}
120+
121+
/**
122+
* @param string|array|null $index
123+
*
124+
* @return list<string>
125+
*/
126+
private function normalizeIndex(string|array|null $index): array
127+
{
128+
if ($index === null) {
129+
return [];
130+
}
131+
132+
if (is_array($index)) {
133+
return array_values($index);
134+
}
135+
136+
return [$index];
137+
}
138+
139+
private function assertLookupMethod(string $lookupMethod): void
140+
{
141+
if (!method_exists($this->objectRepository, $lookupMethod)) {
142+
throw new \InvalidArgumentException(
143+
sprintf(
144+
'Repository %s has no method %s',
145+
get_class($this->objectRepository),
146+
$lookupMethod
147+
)
148+
);
149+
}
150+
}
151+
}

0 commit comments

Comments
 (0)