Skipping duplicate contacts during import using return false causes entire import to fail

Hi everyone,

I’m working with PHPMaker 2025.11 and trying to skip duplicate contacts during the import process using the Row_Import event.

Here’s what I’m trying to achieve:

  • Each contact has a LinkedIn field.

  • If the LinkedIn URL already exists in the contacts table, the import should skip that single row silently — without stopping the entire import process.

  • For missing required fields (like Firstname, Lastname, EntityId, or ContactownerId), I still want to throw an exception to cancel that row.

I tried the following logic inside Row_Import:

$exists = $conn->fetchOne(
    'SELECT 1 FROM contacts WHERE LOWER(Linkedin) = LOWER(?)',
    [$row['Linkedin']]
);

if ($exists) {
    return false; // Skip the row if LinkedIn already exists
}

I also disabled “Import with transaction” in the Advanced Settings.

However, when I do this:

  • Any row that returns false is counted as a failure, which is expected.
  • The other rows are still correctly detected and processed by Row_Import — PHPMaker shows them as “green” in the preview.
  • But because one of the rows returns false, I am not allowed to proceed with the actual import at all.
  • So in the end, none of the records are inserted, even though the others are perfectly valid.

My question:

Why does return false in Row_Import cause the whole import to stop, even when transactions are disabled?

Is there a better way to skip individual duplicates without breaking the entire import?

Full code:

// Row Import event
function Row_Import(array &$row, int $count): bool
{
    // Transaction nicht mehr notwendig, weil es dafür eine eigene option in den advanced settings gibt


    //Log($count); // Import record count
    //var_dump($row); // Import row
    //return false; // Return false to skip import

    global $conn;
    $conn = $GLOBALS["Conn"]; // DB-Verbindung holen

    // CSV-Header → DB-Felder mappen (ohne Note, Tags, Dateofbirth)
    $row['Lastname']       = trim($row['Last Name']  ?? '');
    $row['Firstname']      = trim($row['First Name'] ?? '');

    if ($row['Firstname'] === '' || $row['Lastname'] === '') {
        throw new \Port\Exception\UnexpectedValueException(
            "Missing first name or last name"
        );
        return false; // Diese Zeile wird nicht importiert
    }

    $row['Nickname']       = '';

    // *********************************************************
    // EntityId über Firmenname ermitteln oder neue Firma anlegen
    // *********************************************************
    $company               = trim($row['Company'] ?? '');
    $entityId = null;

    if ($company !== '') {
        $entityId = $conn->fetchOne('SELECT EntityId FROM entities WHERE LOWER(Name) = LOWER(?)', [$company]);

        if (!$entityId) {
            $conn->executeStatement(
                'INSERT INTO entities
                (Name, Own, NotesFromEntities, NotesFromContacts, NotesFromProjects, AddedTs)
                VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)',
                [$company, 0, 0, 0, 0]
            );
            $entityId = $conn->lastInsertId();
        }

        $row['EntityId'] = $entityId;
    } else {
        throw new \Port\Exception\UnexpectedValueException(
            "Missing company name for {$row['Firstname']} {$row['Lastname']}"
        );
        return false; // Diese Zeile wird nicht importiert
    }

    // *********************************************************
    // Duplikatcheck über LinkedIn
    // *********************************************************
    $exists = $conn->fetchOne(
        'SELECT 1 FROM contacts WHERE LOWER(Linkedin) = LOWER(?)',
        [$row['Linkedin']]
    );

    if ($exists) {
        // Duplikat → überspringen (aber erfolgreich)
        //$row = [];
        //$row = array_intersect_key($row, array_flip(['Firstname', 'Lastname', 'EntityId']));
        //return true;

        return false;
    }

    $row['Entityposition'] = trim($row['Job Title']  ?? '');

    //$sql = "SELECT ContactId FROM users WHERE Username = ?";
    //$contactId = $conn->executeQuery($sql, [CurrentUserName()])->fetchOne();
    //$row['ContactownerId'] = $contactId;

    // *********************************************************
    // Userermittlung
    // *********************************************************
    $defaultOwnerId = $conn->fetchOne("
        SELECT Value FROM customizing_table
        WHERE Name = 'ContactownerId import'
    ");

    $contactId = is_numeric($defaultOwnerId) ? (int)$defaultOwnerId : 0;

    if ($contactId <= 0) {
        throw new \Port\Exception\UnexpectedValueException(
            "Missing or invalid ContactOwnerId in customizing_table"
        );
        return false;
    }

    $row['ContactownerId'] = $contactId;

  
    $row['Email']          = trim($row['Email']      ?? '');
    $row['Phone']          = trim($row['Phone']      ?? '');

    $row['Linkedin'] = trim($row['Linkedin'] ?? '');
    if ($row['Linkedin'] === '') {
        $row['Linkedin'] = 'NOT AVAILABLE';
    }



    // Standard-TagId(s) für Import aus customizing_table holen
    $defaultTags = $conn->fetchOne("
        SELECT Value FROM customizing_table
        WHERE Name = 'DefaultImportContactTagId'
        AND Active = '1'
    ");

    if ($defaultTags !== '0') {
       $row['Tags'] = $defaultTags;
    } else {
        $row['Tags'] = ''; // leer lassen, wenn kein gültiger Wert gefunden
    }

    




    $row['WebsiteLinks']   = '';
    $row['SharePointLinks']= '';

    // Nur erlaubte Felder behalten – Rest automatisch entfernen
    $allowedFields = [
        'Lastname',
        'Firstname',
        'Nickname',
        'EntityId',
        'Entityposition',
        'ContactownerId',
        'Email',
        'Phone',
        'Linkedin',
        'Tags',
        'WebsiteLinks',
        'SharePointLinks'
    ];

    $row = array_intersect_key($row, array_flip($allowedFields));


    // Neuer Kontakt → Insert zulassen
    return true;
}

Note that there is an advanced setting Import maximum number of failures:

Maximum number of failures allowed (per file) during import. If Import records with transaction is enabled, this settings sets the maximum failures you allow during the transaction. When the number of failures is exceeded, the transaction will be rollbacked. Default is 0, that is, import will be rollbacked once a failure occurs. Set this value to larger than 0 if you allow more than one failures.

1 Like

Many thanks, I’ve been searching for this for a while!