*/ public static function scopes(): array { return array(self::SCOPE_REDIRECTS, self::SCOPE_CAPTURED); } /** * The statuses a scope covers, mapped to the bucket each one feeds. * * @param string $scope One of the SCOPE_* constants. * @return array status constant => bucket name. Empty for an unknown scope. */ public static function bucketsByStatus(string $scope): array { if ($scope === self::SCOPE_REDIRECTS) { return array( ABJ404_STATUS_MANUAL => 'manual', ABJ404_STATUS_AUTO => 'auto', ABJ404_STATUS_REGEX => 'regex', ); } if ($scope === self::SCOPE_CAPTURED) { return array( ABJ404_STATUS_CAPTURED => 'captured', ABJ404_STATUS_IGNORED => 'ignored', ABJ404_STATUS_LATER => 'later', ); } return array(); } /** * Every bucket name a scope's cached count array carries, in cache order. * * @param string $scope One of the SCOPE_* constants. * @return array */ public static function bucketNames(string $scope): array { $names = array(self::BUCKET_ALL); foreach (self::bucketsByStatus($scope) as $bucket) { $names[] = $bucket; } $names[] = self::BUCKET_TRASH; return $names; } /** * A scope's count array with every bucket at zero. Used as the shape a * failed or empty aggregate falls back to, and as the delta accumulator. * * @param string $scope One of the SCOPE_* constants. * @return array */ public static function zeroCounts(string $scope): array { return array_fill_keys(self::bucketNames($scope), 0); } /** * The count deltas implied by a set of rows moving from one (status, * disabled) distribution to another. * * Histograms rather than individual rows, so one bulk mutation costs the * same two GROUP BY reads as a single-row one. A row that only exists in * `$before` was deleted; one that only exists in `$after` was inserted; * one that appears in both under different keys changed status or was * trashed / restored. * * @param array $before * @param array $after * @return array> scope => bucket => signed delta. * Scopes whose buckets all cancel out are omitted. */ public static function delta(array $before, array $after): array { $deltas = array(); foreach (self::scopes() as $scope) { $deltas[$scope] = self::zeroCounts($scope); } foreach ($before as $row) { self::accumulate($deltas, $row, -1); } foreach ($after as $row) { self::accumulate($deltas, $row, 1); } foreach ($deltas as $scope => $buckets) { if (count(array_filter($buckets)) === 0) { unset($deltas[$scope]); } } return $deltas; } /** * The delta for a known number of rows entering (sign 1) or leaving * (sign -1) one exact (status, disabled) cell. Callers that already know * what they changed use this instead of reading a histogram. * * @param int $status * @param int $disabled * @param int $count Number of rows, always positive. * @param int $sign 1 for rows added, -1 for rows removed. * @return array> */ public static function deltaForRows(int $status, int $disabled, int $count, int $sign): array { $row = array('status' => $status, 'disabled' => $disabled, 'count' => abs($count)); return $sign < 0 ? self::delta(array($row), array()) : self::delta(array(), array($row)); } /** * Fold one histogram cell into the accumulating deltas. * * @param array> $deltas Modified in place. * @param array{status:int,disabled:int,count:int} $row * @param int $sign */ private static function accumulate(array &$deltas, array $row, int $sign): void { $status = isset($row['status']) ? (int)$row['status'] : -1; $disabled = isset($row['disabled']) ? (int)$row['disabled'] : -1; $count = isset($row['count']) ? (int)$row['count'] : 0; if ($count <= 0) { return; } $signed = $sign * $count; foreach (self::scopes() as $scope) { $statusBuckets = self::bucketsByStatus($scope); if (!isset($statusBuckets[$status])) { continue; } if ($disabled === 0) { $deltas[$scope][self::BUCKET_ALL] += $signed; $deltas[$scope][$statusBuckets[$status]] += $signed; } else if ($disabled === 1) { $deltas[$scope][self::BUCKET_TRASH] += $signed; } } } }