Skip to main content

Import a subscriber with their original join date

Premium Intermediate

A migration importing an established audience arrives holding a join date per subscriber. Without carrying it across, every imported token is stamped at import time and TenureBadgeCalculator — which derives tenure from the earliest created_at across a user’s active tokens — badges a five-year supporter as a New Member. That is worse than showing nothing: it is confidently wrong, in front of the subscriber it is wrong about.

TokenManager::generate() and TokenRepository::insert() both take an optional trailing ?string $created_at. Leave it null (every non-migration caller) and the column’s DEFAULT CURRENT_TIMESTAMP applies. Pass a Y-m-d H:i:s or bare Y-m-d string in site-local time and it is written instead — the bare form widens to midnight, which is the shape a CSV export actually has.

Validate the whole file before you write a single row. generate() throws InvalidArgumentException out of the guard on a bad date, and the plaintext token generated for that attempt is discarded with the exception. BackdateGuard::reason() is the non-throwing half and exists precisely so an importer can collect every bad row and show the operator one list, rather than aborting on row 7 of 400.

Code

<?php
use Benecaster\Token\BackdateGuard;
use Benecaster\Token\TokenManager;

/**
 * @param array<int, array{user_id: int, join_date: string}> $rows Parsed CSV.
 * @return array{imported: int, rejected: array<int, array{user_id: int, join_date: string, reason: string}>}
 */
function my_addon_import_with_tenure( TokenManager $manager, array $rows, int $show_id, string $tier_slug ): array {
    $guard    = new BackdateGuard();
    $good     = [];
    $rejected = [];

    // Pass one: judge every row before writing any of them. reason() returns
    // null when the date is fine, otherwise a stable code — 'unparseable',
    // 'future' or 'before_floor' — that you can group and translate yourself.
    foreach ( $rows as $row ) {
        $reason = $guard->reason( $row['join_date'] );

        if ( null === $reason ) {
            $good[] = $row;
            continue;
        }

        $rejected[] = $row + [ 'reason' => $reason ];
    }

    // Show the operator the rejects BEFORE importing. A half-finished
    // migration is much harder to reason about than one that has not started.
    if ( $rejected !== [] ) {
        my_addon_report_bad_rows( $rejected );
    }

    $imported = 0;
    foreach ( $good as $row ) {
        $token = $manager->generate(
            $row['user_id'],
            $show_id,
            $tier_slug,
            'subscriber',
            $row['join_date']   // already validated above
        );

        my_addon_email_feed_url( $row['user_id'], $token );
        $imported++;
    }

    return [ 'imported' => $imported, 'rejected' => $rejected ];
}

View on GitHub →

Need this built rather than just documented? See our services →