Skip to content
Merged
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
61 changes: 38 additions & 23 deletions src/DoctrineWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Doctrine\Persistence\ObjectManager;
use Doctrine\Persistence\ObjectRepository;
use Port\Doctrine\Exception\UnsupportedDatabaseTypeException;
use Port\Doctrine\LookupStrategy\FieldsLookupStrategy;
use Port\Writer;

/**
Expand Down Expand Up @@ -72,25 +73,46 @@ class DoctrineWriter implements Writer, Writer\FlushableWriter
/**
* Method used for looking up the item
*
* @var array
* @var array|callable
*/
protected $lookupMethod;

/**
* Strategy used to look up existing entities when truncate is disabled.
*/
private LookupStrategy $lookupStrategy;

private Inflector $inflector;

/**
* Create a Doctrine writer with a custom object lookup strategy.
*
* Prefer this when you need QueryBuilder-based or other non-field lookups
* (see https://github.com/portphp/doctrine/issues/3).
*/
public static function withLookupStrategy(
ObjectManager $objectManager,
string $objectName,
LookupStrategy $lookupStrategy
): self {
return new self($objectManager, $objectName, null, 'findOneBy', $lookupStrategy);
}

/**
* Constructor
*
* @param ObjectManager $objectManager
* @param string $objectName
* @param string|array $index Field or fields to find current entities by
* @param string $lookupMethod Method used for looking up the item
* @param ObjectManager $objectManager
* @param string $objectName
* @param string|array|null $index Field or fields to find current entities by
* @param string $lookupMethod Method used for looking up the item
* @param LookupStrategy|null $lookupStrategy Optional custom strategy (overrides $index / $lookupMethod)
*/
public function __construct(
ObjectManager $objectManager,
$objectName,
$index = null,
$lookupMethod = 'findOneBy'
$lookupMethod = 'findOneBy',
?LookupStrategy $lookupStrategy = null
) {
$this->ensureSupportedObjectManager($objectManager);
$this->objectManager = $objectManager;
Expand All @@ -116,6 +138,11 @@ public function __construct(
);
}
$this->lookupMethod = [$this->objectRepository, $lookupMethod];
$this->lookupStrategy = $lookupStrategy ?? new FieldsLookupStrategy(
$this->objectRepository,
$index,
$lookupMethod
);
$this->inflector = InflectorFactory::create()->build();
}

Expand Down Expand Up @@ -308,27 +335,15 @@ protected function reEnableLogging()

protected function findOrCreateItem(array $item): object
{
$object = null;
// If the table was not truncated to begin with, find current object
// first
// If the table was not truncated to begin with, find current object first
if (!$this->truncate) {
if (!empty($this->lookupFields)) {
$lookupConditions = [];
foreach ($this->lookupFields as $fieldName) {
$lookupConditions[$fieldName] = $item[$fieldName];
}

$object = call_user_func($this->lookupMethod, $lookupConditions);
} else {
$object = $this->objectRepository->find(current($item));
$object = $this->lookupStrategy->lookup($item);
if ($object !== null) {
return $object;
}
}

if (!$object) {
return $this->getNewInstance();
}

return $object;
return $this->getNewInstance();
}

protected function ensureSupportedObjectManager(ObjectManager $objectManager)
Expand Down
21 changes: 21 additions & 0 deletions src/LookupStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Port\Doctrine;

/**
* Finds existing objects in the database for upsert-style writes.
*
* Implement this interface to customize how {@see DoctrineWriter} looks up
* entities (e.g. via QueryBuilder or custom repository methods).
*/
interface LookupStrategy
{
/**
* Look up an existing object for the given import item.
*
* @param array $item Import item (field => value)
*
* @return object|null Null if no object was found (writer will create a new instance)
*/
public function lookup(array $item): ?object;
}
151 changes: 151 additions & 0 deletions src/LookupStrategy/FieldsLookupStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

namespace Port\Doctrine\LookupStrategy;

use Doctrine\Persistence\ObjectManager;
use Doctrine\Persistence\ObjectRepository;
use Port\Doctrine\LookupStrategy;

/**
* Default lookup strategy using repository field criteria.
*
* Behavior matches historical DoctrineWriter lookup:
* - With lookup fields: call the repository method (default findOneBy) with those criteria
* - Without lookup fields: ObjectRepository::find(current($item))
*/
class FieldsLookupStrategy implements LookupStrategy
{
private ObjectRepository $objectRepository;

/** @var list<string> */
private array $lookupFields;

private string $lookupMethod;

/**
* @param ObjectRepository $objectRepository
* @param string|array|null $index Field or fields used as lookup criteria (null = find by first item value)
* @param string $lookupMethod Repository method used when lookup fields are set
*/
public function __construct(
ObjectRepository $objectRepository,
string|array|null $index = null,
string $lookupMethod = 'findOneBy'
) {
$this->objectRepository = $objectRepository;
$this->lookupFields = $this->normalizeIndex($index);
$this->assertLookupMethod($lookupMethod);
$this->lookupMethod = $lookupMethod;
}

/**
* Convenience factory from an object manager and class name.
*/
public static function fromObjectManager(
ObjectManager $objectManager,
string $objectName,
string|array|null $index = null,
string $lookupMethod = 'findOneBy'
): self {
return new self(
$objectManager->getRepository($objectName),
$index,
$lookupMethod
);
}

/**
* @param string $field Field to find current objects by
*/
public function withLookupField(string $field): self
{
return $this->withLookupFields([$field]);
}

/**
* @param list<string> $fields Fields to find current objects by
*/
public function withLookupFields(array $fields): self
{
$new = clone $this;
$new->lookupFields = array_values($fields);

return $new;
}

/**
* Accept string or list of fields (mirrors DoctrineWriter $index constructor arg).
*
* @param string|array $index
*/
public function withIndex(string|array $index): self
{
if (is_array($index)) {
return $this->withLookupFields($index);
}

return $this->withLookupField($index);
}

/**
* Doctrine repository method for finding objects when lookup fields are set.
*/
public function withLookupMethod(string $lookupMethod): self
{
$this->assertLookupMethod($lookupMethod);

$new = clone $this;
$new->lookupMethod = $lookupMethod;

return $new;
}

public function lookup(array $item): ?object
{
if (!empty($this->lookupFields)) {
$lookupConditions = [];
foreach ($this->lookupFields as $fieldName) {
$lookupConditions[$fieldName] = $item[$fieldName] ?? null;
}

$result = $this->objectRepository->{$this->lookupMethod}($lookupConditions);

return is_object($result) ? $result : null;
}

$result = $this->objectRepository->find(current($item));

return is_object($result) ? $result : null;
}

/**
* @param string|array|null $index
*
* @return list<string>
*/
private function normalizeIndex(string|array|null $index): array
{
if ($index === null) {
return [];
}

if (is_array($index)) {
return array_values($index);
}

return [$index];
}

private function assertLookupMethod(string $lookupMethod): void
{
if (!method_exists($this->objectRepository, $lookupMethod)) {
throw new \InvalidArgumentException(
sprintf(
'Repository %s has no method %s',
get_class($this->objectRepository),
$lookupMethod
)
);
}
}
}
Loading