'integer', 'target_id' => 'integer', 'muted' => 'integer', ]; /** * The ids of everyone following this target and still listening. * * The one read the dispatcher makes per event. Returns ids rather than rows * because that is all a recipient list is, and loading follow rows to throw * away every column but one would make a hot path cost more than it earns. * * @return array */ public static function followerIdsFor(string $targetType, int $targetId): array { $rows = static::select(['user_id']) ->where('target_type', $targetType) ->where('target_id', $targetId) ->where('muted', 0) ->get(); return array_values( array_unique( array_map(static fn ($row): int => (int) $row->user_id, self::asList($rows)) ) ); } /** * Everyone with a row on this target, muted or not. * * For moving follows from one term to another, where a muted row is as * much a decision as a listening one and has to travel with the rest. * * @return array */ public static function userIdsFor(string $targetType, int $targetId): array { $rows = static::select(['user_id']) ->where('target_type', $targetType) ->where('target_id', $targetId) ->get(); return array_values( array_unique( array_map(static fn ($row): int => (int) $row->user_id, self::asList($rows)) ) ); } /** * Whatever get() answered, as a list of rows. * * A limit-1 result that matched exactly one row comes back as a bare Model * rather than a list — see the note on findFor(). The plain array cast this * replaces would have turned that one Model into its own property list * without saying so. * * @param mixed $result * * @return array */ public static function asList($result): array { // get() answers with a Collection, which is neither a Model nor an // array — so every row fell through to the is_array() branch below and // a full page came back as an empty list. all() unwraps it. if ($result instanceof Collection) { return array_values($result->all()); } if ($result instanceof Model) { return [$result]; } return \is_array($result) ? array_values($result) : []; } /** * One member's follow of one thing, muted or not. * * Returns muted rows too, on purpose: the caller asking "is this followed?" * needs to tell "never followed" from "followed and silenced", because those * are different buttons. * * Not named find(): QueryBuilder::find($attributes) already exists and * reaches models through __callStatic, so a same-named static here would * shadow it with an incompatible signature for every caller. * * @return null|object one row, with its columns as properties */ public static function findFor(int $userId, string $targetType, int $targetId) { // first(), not take(1)->get(): since wp-database 2.0, get() always answers // with a Collection, so the old "is it a Model?" test never matched and // this said "not following" about every row — unfollow did nothing and a // second follow tried to insert a duplicate. $found = static::select(['*']) ->where('user_id', $userId) ->where('target_type', $targetType) ->where('target_id', $targetId) ->first(); return $found instanceof Model ? $found : null; } /** * The ids of one kind of thing a member follows and has not muted. * * @return int[] */ public static function activeTargetIds(int $userId, string $targetType): array { $rows = static::select(['target_id']) ->where('user_id', $userId) ->where('target_type', $targetType) ->where('muted', 0) ->get(); return array_map(static fn ($row): int => (int) $row->target_id, self::asList($rows ?? [])); } /** * Everything one member follows and has not muted, newest first. * * @return array{data: array, total: int, pages: int} */ public static function activeFor(int $userId, string $targetType, int $page, int $perPage) { $result = static::select(['*']) ->where('user_id', $userId) ->where('target_type', $targetType) ->where('muted', 0) ->orderBy('created_at') ->desc() ->paginate($page, $perPage); // A per-page of 1 comes back as a bare Model, not a list — paginate() // forwards it to take(). See asList(). $result['data'] = self::asList($result['data'] ?? []); return $result; } }