[TOOLS] Continue raising PHPStan level to 6

v3
Hugo Sales 1 year ago
parent c31f3d4997
commit 2fd46ca886
No known key found for this signature in database
GPG Key ID: 7D0C7EAFC9D835A0

@ -178,7 +178,7 @@ return $config
// There MUST NOT be a space after the opening parenthesis. There MUST NOT be a space before the closing parenthesis.
'no_spaces_inside_parenthesis' => true,
// Removes `@param`, `@return` and `@var` tags that don't provide any useful information.
'no_superfluous_phpdoc_tags' => true,
'no_superfluous_phpdoc_tags' => false,
// Remove trailing commas in list function calls.
'no_trailing_comma_in_list_call' => true,
// PHP single-line arrays should not have trailing comma.

@ -54,7 +54,7 @@ use Symfony\Component\HttpFoundation\Request;
*/
class Circle extends Component
{
/** @phpstan-use MetaCollectionTrait<Circle> */
/** @phpstan-use MetaCollectionTrait<ActorCircle> */
use MetaCollectionTrait;
public const TAG_CIRCLE_REGEX = '/' . Nickname::BEFORE_MENTIONS . '@#([\pL\pN_\-\.]{1,64})/';
protected const SLUG = 'circle';
@ -228,7 +228,7 @@ class Circle extends Component
* @param null|array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
* @param bool $ids_only true if only the Collections ids are to be returned
*
* @return Circle[]|int[]
* @return ($ids_only is true ? int[] : ActorCircle[])
*/
protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array
{

@ -90,14 +90,12 @@ class Circles extends MetaCollectionController
return $this->getCollectionItems($tagger_id, $circle_id);
}
/**
* @return ActorCircle[]
*/
public function getCollectionsByActorId(int $owner_id): array
{
return DB::findBy(ActorCircle::class, ['tagger' => $owner_id], order_by: ['id' => 'desc']);
}
public function getCollectionBy(int $owner_id, int $collection_id): ActorCircle
public function getCollectionBy(int $owner_id, int $collection_id): self
{
return DB::findOneBy(ActorCircle::class, ['id' => $collection_id, 'actor_id' => $owner_id]);
}

@ -26,7 +26,8 @@ class Collection extends Component
*
* @param array<string, OrderByType> $note_order_by
* @param array<string, OrderByType> $actor_order_by
* @return array{notes: Note[], actors: Actor[]}
*
* @return array{notes: null|Note[], actors: null|Actor[]}
*/
public static function query(string $query, int $page, ?string $locale = null, ?Actor $actor = null, array $note_order_by = [], array $actor_order_by = []): array
{
@ -84,6 +85,9 @@ class Collection extends Component
/**
* Convert $term to $note_expr and $actor_expr, search criteria. Handles searching for text
* notes, for different types of actors and for the content of text notes
*
* @param mixed $note_expr
* @param mixed $actor_expr
*/
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
{
@ -91,50 +95,50 @@ class Collection extends Component
$term = explode(':', $term);
if (Formatting::startsWith($term[0], 'note')) {
switch ($term[0]) {
case 'notes-all':
$note_expr = $eb->neq('note.created', null);
break;
case 'note-local':
$note_expr = $eb->eq('note.is_local', filter_var($term[1], \FILTER_VALIDATE_BOOLEAN));
break;
case 'note-types':
case 'notes-include':
case 'note-filter':
if (\is_null($note_expr)) {
$note_expr = [];
}
if (array_intersect(explode(',', $term[1]), ['text', 'words']) !== []) {
$note_expr[] = $eb->neq('note.content', null);
} else {
$note_expr[] = $eb->eq('note.content', null);
}
break;
case 'note-conversation':
$note_expr = $eb->eq('note.conversation_id', (int) trim($term[1]));
break;
case 'note-from':
case 'notes-from':
$subscribed_expr = $eb->eq('subscription.subscriber_id', $actor->getId());
$type_consts = [];
if ($term[1] === 'subscribed') {
$type_consts = null;
}
foreach (explode(',', $term[1]) as $from) {
if (str_starts_with($from, 'subscribed-')) {
[, $type] = explode('-', $from);
if (\in_array($type, ['actor', 'actors'])) {
$type_consts = null;
} else {
$type_consts[] = \constant(Actor::class . '::' . mb_strtoupper($type === 'organisation' ? 'group' : $type));
case 'notes-all':
$note_expr = $eb->neq('note.created', null);
break;
case 'note-local':
$note_expr = $eb->eq('note.is_local', filter_var($term[1], \FILTER_VALIDATE_BOOLEAN));
break;
case 'note-types':
case 'notes-include':
case 'note-filter':
if (\is_null($note_expr)) {
$note_expr = [];
}
if (array_intersect(explode(',', $term[1]), ['text', 'words']) !== []) {
$note_expr[] = $eb->neq('note.content', null);
} else {
$note_expr[] = $eb->eq('note.content', null);
}
break;
case 'note-conversation':
$note_expr = $eb->eq('note.conversation_id', (int) trim($term[1]));
break;
case 'note-from':
case 'notes-from':
$subscribed_expr = $eb->eq('subscription.subscriber_id', $actor->getId());
$type_consts = [];
if ($term[1] === 'subscribed') {
$type_consts = null;
}
foreach (explode(',', $term[1]) as $from) {
if (str_starts_with($from, 'subscribed-')) {
[, $type] = explode('-', $from);
if (\in_array($type, ['actor', 'actors'])) {
$type_consts = null;
} else {
$type_consts[] = \constant(Actor::class . '::' . mb_strtoupper($type === 'organisation' ? 'group' : $type));
}
}
}
}
if (\is_null($type_consts)) {
$note_expr = $subscribed_expr;
} elseif (!empty($type_consts)) {
$note_expr = $eb->andX($subscribed_expr, $eb->in('note_actor.type', $type_consts));
}
break;
if (\is_null($type_consts)) {
$note_expr = $subscribed_expr;
} elseif (!empty($type_consts)) {
$note_expr = $eb->andX($subscribed_expr, $eb->in('note_actor.type', $type_consts));
}
break;
}
} elseif (Formatting::startsWith($term, 'actor-')) {
switch ($term[0]) {
@ -148,8 +152,8 @@ class Collection extends Component
foreach (
[
Actor::PERSON => ['person', 'people'],
Actor::GROUP => ['group', 'groups', 'org', 'orgs', 'organisation', 'organisations', 'organization', 'organizations'],
Actor::BOT => ['bot', 'bots'],
Actor::GROUP => ['group', 'groups', 'org', 'orgs', 'organisation', 'organisations', 'organization', 'organizations'],
Actor::BOT => ['bot', 'bots'],
] as $type => $match) {
if (array_intersect(explode(',', $term[1]), $match) !== []) {
$actor_expr[] = $eb->eq('actor.type', $type);

@ -4,6 +4,9 @@ declare(strict_types = 1);
namespace Component\Collection\Util\Controller;
/**
* @extends OrderedCollection<\Component\Circle\Entity\ActorCircle>
*/
class CircleController extends OrderedCollection
{
}

@ -6,18 +6,20 @@ namespace Component\Collection\Util\Controller;
use App\Core\Controller;
use App\Entity\Actor;
use App\Entity\Note;
use App\Util\Common;
use Component\Collection\Collection as CollectionComponent;
/**
* @template T
*/
class Collection extends Controller
abstract class Collection extends Controller
{
/**
* @param array<string, OrderByType> $note_order_by
* @param array<string, OrderByType> $actor_order_by
* @return array<T>
*
* @return array{notes: null|Note[], actors: null|Actor[]}
*/
public function query(string $query, ?string $locale = null, ?Actor $actor = null, array $note_order_by = [], array $actor_order_by = []): array
{

@ -38,6 +38,11 @@ use App\Entity\Note;
use App\Util\Common;
use Functional as F;
/**
* @template T
*
* @extends OrderedCollection<T>
*/
abstract class FeedController extends OrderedCollection
{
/**
@ -45,9 +50,11 @@ abstract class FeedController extends OrderedCollection
* notes or actors the user specified, as well as format the raw
* list of notes into a usable format
*
* @template T of Note|Actor
* @param T[] $result
* @return T[]
* @template NA of Note|Actor
*
* @param NA[] $result
*
* @return NA[]
*/
protected function postProcess(array $result): array
{

@ -39,10 +39,13 @@ use App\Util\Common;
use App\Util\Exception\RedirectException;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpFoundation\Request;
/**
* @template T
* @template T of object
*
* @extends FeedController<T>
*/
abstract class MetaCollectionController extends FeedController
{
@ -70,7 +73,7 @@ abstract class MetaCollectionController extends FeedController
abstract public function createCollection(int $owner_id, string $name): bool;
/**
* @return T[]
* @return ControllerResultType
*/
public function collectionsViewByActorNickname(Request $request, string $nickname): array
{
@ -79,7 +82,7 @@ abstract class MetaCollectionController extends FeedController
}
/**
* @return T[]
* @return ControllerResultType
*/
public function collectionsViewByActorId(Request $request, int $id): array
{
@ -135,34 +138,23 @@ abstract class MetaCollectionController extends FeedController
// the functions and passing that class to the template.
// This is suggested at https://web.archive.org/web/20220226132328/https://stackoverflow.com/questions/3595727/twig-pass-function-into-template/50364502
$fn = new class($id, $nickname, $request, $this, static::SLUG) {
private $id;
private $nick;
private $request;
private $parent;
private $slug;
public function __construct($id, $nickname, $request, $parent, $slug)
public function __construct(private int $id, private string $nickname, private Request $request, private object $parent, private string $slug)
{
$this->id = $id;
$this->nick = $nickname;
$this->request = $request;
$this->parent = $parent;
$this->slug = $slug;
}
// there's already an injected function called path,
// that maps to Router::url(name, args), but since
// I want to preserve nicknames, I think it's better
// to use that getUrl function
public function getUrl($cid)
public function getUrl(int $cid): string
{
return $this->parent->getCollectionUrl($this->id, $this->nick, $cid);
return $this->parent->getCollectionUrl($this->id, $this->nickname, $cid);
}
// There are many collections in this page and we need two
// forms for each one of them: one form to edit the collection's
// name and another to remove the collection.
// creating the edit form
public function editForm($collection)
public function editForm(object $collection): FormView
{
$edit = Form::create([
['name', TextType::class, [
@ -181,7 +173,7 @@ abstract class MetaCollectionController extends FeedController
]);
$edit->handleRequest($this->request);
if ($edit->isSubmitted() && $edit->isValid()) {
$this->parent->setCollectionName($this->id, $this->nick, $collection, $edit->getData()['name']);
$this->parent->setCollectionName($this->id, $this->nickname, $collection, $edit->getData()['name']);
DB::flush();
throw new RedirectException();
}
@ -189,7 +181,7 @@ abstract class MetaCollectionController extends FeedController
}
// creating the remove form
public function rmForm($collection)
public function rmForm(object $collection): FormView
{
$rm = Form::create([
['remove_' . $collection->getId(), SubmitType::class, [
@ -202,7 +194,7 @@ abstract class MetaCollectionController extends FeedController
]);
$rm->handleRequest($this->request);
if ($rm->isSubmitted()) {
$this->parent->removeCollection($this->id, $this->nick, $collection);
$this->parent->removeCollection($this->id, $this->nickname, $collection);
DB::flush();
throw new RedirectException();
}
@ -220,12 +212,18 @@ abstract class MetaCollectionController extends FeedController
];
}
/**
* @return ControllerResultType
*/
public function collectionsEntryViewNotesByNickname(Request $request, string $nickname, int $cid): array
{
$user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]);
return self::collectionsEntryViewNotesByActorId($request, $user->getId(), $cid);
}
/**
* @return ControllerResultType
*/
public function collectionsEntryViewNotesByActorId(Request $request, int $id, int $cid): array
{
$collection = $this->getCollectionBy($id, $cid);

@ -4,6 +4,11 @@ declare(strict_types = 1);
namespace Component\Collection\Util\Controller;
class OrderedCollection extends Collection
/**
* @template T
*
* @extends Collection<T>
*/
abstract class OrderedCollection extends Collection
{
}

@ -56,9 +56,9 @@ trait MetaCollectionTrait
/**
* create a collection owned by Actor $owner.
*
* @param Actor $owner The collection's owner
* @param array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
* @param string $name Collection's name
* @param Actor $owner The collection's owner
* @param array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
* @param string $name Collection's name
*/
abstract protected function createCollection(Actor $owner, array $vars, string $name): void;
/**
@ -82,6 +82,7 @@ trait MetaCollectionTrait
/**
* Check the route to determine whether the widget should be added
*
* @param array<string, mixed> $vars
*/
abstract protected function shouldAddToRightPanel(Actor $user, array $vars, Request $request): bool;
@ -91,7 +92,8 @@ trait MetaCollectionTrait
* @param Actor $owner Collection's owner
* @param null|array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
* @param bool $ids_only if true, the function must return only the primary key or each collections
* @return T[]|int[]
*
* @return int[]|T[]
*/
abstract protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array;
@ -101,7 +103,7 @@ trait MetaCollectionTrait
* the current item to, and another to create a new collection.
*
* @param array<string, mixed> $vars
* @param string[] $res
* @param string[] $res
*/
public function onAppendRightPanelBlock(Request $request, array $vars, array &$res): EventResult
{
@ -146,7 +148,7 @@ trait MetaCollectionTrait
if ($add_form->isSubmitted() && $add_form->isValid()) {
$selected = $add_form->getData()['collections'];
$removed = array_filter($already_selected, fn ($x) => !\in_array($x, $selected));
$added = array_filter($selected, fn ($x) => !\in_array($x, $already_selected));
$added = array_filter($selected, fn ($x) => !\in_array($x, $already_selected));
if (\count($removed) > 0) {
$this->removeItem($user, $vars, $removed, $collections);
}
@ -196,7 +198,7 @@ trait MetaCollectionTrait
}
/**
* @param string[]
* @param string[] $styles
*/
public function onEndShowStyles(array &$styles, string $route): EventResult
{

@ -32,6 +32,9 @@ abstract class Parser
{
/**
* Merge $parts into $criteria_arr
*
* @param mixed[] $parts
* @param Criteria[] $criteria_arr
*/
private static function connectParts(array &$parts, array &$criteria_arr, string $last_op, mixed $eb, bool $force = false): void
{

@ -46,6 +46,9 @@ use Component\Conversation\Entity\ConversationMute;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class Conversation extends FeedController
{
/**
@ -55,7 +58,10 @@ class Conversation extends FeedController
*
* @throws \App\Util\Exception\ServerException
*
* @return array Array containing keys: 'notes' (all known notes in the given Conversation), 'should_format' (boolean, stating if onFormatNoteList events may or not format given notes), 'page_title' (used as the title header)
* @return ControllerResultType Array containing keys: 'notes' (all known
* notes in the given Conversation), 'should_format' (boolean, stating if
* onFormatNoteList events may or not format given notes), 'page_title'
* (used as the title header)
*/
public function showConversation(Request $request, int $conversation_id): array
{
@ -83,7 +89,7 @@ class Conversation extends FeedController
* @throws NoSuchNoteException
* @throws ServerException
*
* @return array
* @return ControllerResultType
*/
public function addReply(Request $request)
{
@ -103,7 +109,7 @@ class Conversation extends FeedController
* @throws \App\Util\Exception\RedirectException
* @throws \App\Util\Exception\ServerException
*
* @return array Array containing templating where the form is to be rendered, and the form itself
* @return ControllerResultType Array containing templating where the form is to be rendered, and the form itself
*/
public function muteConversation(Request $request, int $conversation_id)
{

@ -95,11 +95,13 @@ class Conversation extends Component
* HTML rendering event that adds a reply link as a note
* action, if a user is logged in.
*
* @param \App\Entity\Note $note The Note being rendered
* @param array $actions Contains keys 'url' (linking 'conversation_reply_to'
* route), 'title' (used as title for aforementioned url),
* 'classes' (CSS styling classes used to visually inform the user of action context),
* 'id' (HTML markup id used to redirect user to this anchor upon performing the action)
* @param \App\Entity\Note $note The Note being rendered
* @param array{url: string, title: string, classes: string, id: string} $actions
* Contains keys 'url' (linking 'conversation_reply_to' route),
* 'title' (used as title for aforementioned url), 'classes' (CSS styling
* classes used to visually inform the user of action context), 'id' (HTML
* markup id used to redirect user to this anchor upon performing the
* action)
*
* @throws \App\Util\Exception\ServerException
*/
@ -138,8 +140,11 @@ class Conversation extends Component
/**
* Append on note information about user actions.
*
* @param array $vars Contains information related to Note currently being rendered
* @param array $result Contains keys 'actors', and 'action'. Needed to construct a string, stating who ($result['actors']), has already performed a reply ($result['action']), in the given Note (vars['note'])
* @param array<string, mixed> $vars Contains information related to Note currently being rendered
* @param array{actors: Actor[], action: string} $result
*cContains keys 'actors', and 'action'. Needed to construct a string,
* stating who ($result['actors']), has already performed a reply
* ($result['action']), in the given Note (vars['note'])
*/
public function onAppendCardNote(array $vars, array &$result): EventResult
{
@ -206,7 +211,7 @@ class Conversation extends Component
/**
* Posting event to add extra information to Component\Posting form data
*
* @param array $data Transport data to be filled with reply_to_id
* @param array{reply_to_id: int} $data Transport data to be filled with reply_to_id
*
* @throws \App\Util\Exception\ClientException
* @throws \App\Util\Exception\NoSuchNoteException
@ -224,6 +229,8 @@ class Conversation extends Component
/**
* Add minimal Note card to RightPanel template
*
* @param string[] $elements
*/
public function onPrependPostingForm(Request $request, array &$elements): EventResult
{
@ -250,8 +257,10 @@ class Conversation extends Component
/**
* Adds extra actions related to Conversation Component, that act upon/from the given Note.
*
* @param \App\Entity\Note $note Current Note being rendered
* @param array $actions Containing 'url' (Controller connected route), 'title' (used in anchor link containing the url), ?'classes' (CSS classes required for styling, if needed)
* @param \App\Entity\Note $note Current Note being rendered
* @param array{url: string, title: string, classes?: string} $actions Containing 'url' (Controller connected
* route), 'title' (used in anchor link containing the url), ?'classes' (CSS classes required for styling, if
* needed)
*
* @throws \App\Util\Exception\ServerException
*/

@ -41,10 +41,15 @@ use App\Util\HTML\Heading;
use Component\Collection\Util\Controller\FeedController;
use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class Feeds extends FeedController
{
/**
* The Planet feed represents every local post. Which is what this instance has to share with the universe.
*
* @return ControllerResultType
*/
public function public(Request $request): array
{
@ -60,6 +65,8 @@ class Feeds extends FeedController
/**
* The Home feed represents everything that concerns a certain actor (its subscriptions)
*
* @return ControllerResultType
*/
public function home(Request $request): array
{

@ -244,6 +244,8 @@ class FreeNetwork extends Component
/**
* Add a link header for LRDD Discovery
*
* @param mixed $action
*/
public function onStartShowHTML($action): EventResult
{
@ -344,6 +346,7 @@ class FreeNetwork extends Component
* @param string $preMention Character(s) that signals a mention ('@', '!'...)
*
* @return array the matching URLs (without @ or acct:) and each respective position in the given string
*
* @example.com/mublog/user
*/
public static function extractUrlMentions(string $text, string $preMention = '@'): array
@ -375,6 +378,7 @@ class FreeNetwork extends Component
* @param $mentions
*
* @return bool hook return value
*
* @example.com/mublog/user
*/
public function onEndFindMentions(Actor $sender, string $text, array &$mentions): EventResult
@ -496,6 +500,9 @@ class FreeNetwork extends Component
return Event::next;
}
/**
* @param Actor[] $targets
*/
public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool
{
foreach (self::$protocols as $protocol) {
@ -517,6 +524,9 @@ class FreeNetwork extends Component
/**
* Add fediverse: query expression
* // TODO: adding WebFinger would probably be nice
*
* @param mixed $note_expr
* @param mixed $actor_expr
*/
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
{

@ -61,6 +61,8 @@ class Group extends Controller
*
* @throws RedirectException
* @throws ServerException
*
* @return ControllerResultType
*/
public function groupCreate(Request $request): array
{
@ -89,6 +91,8 @@ class Group extends Controller
* @throws NicknameTooLongException
* @throws NotFoundException
* @throws ServerException
*
* @return ControllerResultType
*/
public function groupSettings(Request $request, int $id): array
{

@ -40,10 +40,15 @@ use Component\Subscription\Entity\ActorSubscription;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class GroupFeed extends FeedController
{
/**
* @throws ServerException
*
* @return ControllerResultType
*/
public function groupView(Request $request, Actor $group): array
{
@ -96,6 +101,8 @@ class GroupFeed extends FeedController
/**
* @throws ClientException
* @throws ServerException
*
* @return ControllerResultType
*/
public function groupViewId(Request $request, int $id): array
{
@ -119,6 +126,8 @@ class GroupFeed extends FeedController
*
* @throws ClientException
* @throws ServerException
*
* @return ControllerResultType
*/
public function groupViewNickname(Request $request, string $nickname): array
{

@ -50,6 +50,8 @@ class Group extends Component
/**
* Enqueues a notification for an Actor (such as person or group) which means
* it shows up in their home feed and such.
*
* @param Actor[] $targets
*/
public function onNewNotificationStart(Actor $sender, Activity $activity, array $targets = [], ?string $reason = null): EventResult
{
@ -70,6 +72,9 @@ class Group extends Component
/**
* Add an <a href=group_actor_settings> to the profile card for groups, if the current actor can access them
*
* @param array<string, mixed> $vars
* @param string[] $res
*/
public function onAppendCardProfile(array $vars, array &$res): EventResult
{
@ -109,6 +114,9 @@ class Group extends Component
return null;
}
/**
* @param Actor[] $targets
*/
public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets): EventResult
{
$group = $this->getGroupFromContext($request);

@ -70,7 +70,7 @@ class Language extends Controller
['actor_id' => $user->getId()],
);
$new_langs = array_udiff($selected_langs, $existing_langs, fn ($l, $r) => $l->getId() <=> $r->getId());
$new_langs = array_udiff($selected_langs, $existing_langs, fn ($l, $r) => $l->getId() <=> $r->getId());
$removing_langs = array_udiff($existing_langs, $selected_langs, fn ($l, $r) => $l->getId() <=> $r->getId());
foreach ($new_langs as $l) {
DB::persist(ActorLanguage::create(['actor_id' => $user->getId(), 'language_id' => $l->getId(), 'ordering' => 0]));
@ -100,6 +100,8 @@ class Language extends Controller
* @throws NoLoggedInUser
* @throws RedirectException
* @throws ServerException
*
* @return ControllerResultType
*/
public function sortLanguages(Request $request): array
{

@ -119,6 +119,9 @@ class ActorLanguage extends Entity
) ?: [Language::getByLocale(Common::config('site', 'language'))];
}
/**
* @return int[]
*/
public static function getActorRelatedLanguagesIds(Actor $actor): array
{
return Cache::getList(

@ -134,6 +134,9 @@ class Language extends Entity
return self::getById($note->getLanguageId());
}
/**
* @return array<string, string>
*/
public static function getLanguageChoices(): array
{
$langs = Cache::getHashMap(
@ -144,6 +147,8 @@ class Language extends Entity
return array_merge(...F\map(array_values($langs), fn ($l) => $l->toChoiceFormat()));
}
/**
* @return array<string, string> */
public function toChoiceFormat(): array
{
return [_m($this->getLongDisplay()) => $this->getLocale()];
@ -152,6 +157,8 @@ class Language extends Entity
/**
* Get all the available languages as well as the languages $actor
* prefers and are appropriate for posting in/to $context_actor
*
* @return array{array<string, string>, array<string, string>}
*/
public static function getSortedLanguageChoices(?Actor $actor, ?Actor $context_actor, ?bool $use_short_display): array
{

@ -45,6 +45,9 @@ class Language extends Component
return Event::next;
}
/**
* @param Note[] $notes
*/
public function onFilterNoteList(?Actor $actor, array &$notes, Request $request): EventResult
{
if (\is_null($actor)) {
@ -60,6 +63,9 @@ class Language extends Component
/**
* Populate $note_expr or $actor_expr with an expression to match a language
*
* @param mixed $note_expr
* @param mixed $actor_expr
*/
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
{

@ -43,6 +43,8 @@ class LeftPanel extends Component
}
/**
* @param array<string, string> $route_params
*
* @throws \App\Util\Exception\DuplicateFoundException
* @throws \App\Util\Exception\ServerException
* @throws ClientException

@ -84,6 +84,8 @@ class NoteToLink extends Entity
* Create an instance of NoteToLink or fill in the
* properties of $obj with the associative array $args. Doesn't
* persist the result
*
* @param (array{link_id: int, note_id: int} & array<string, mixed>) $args
*/
public static function create(array $args, bool $_delegated_call = false): static
{

@ -54,6 +54,8 @@ class Link extends Component
/**
* Extract URLs from $content and create the appropriate Link and NoteToLink entities
*
* @param array{ignoreLinks?: string[]} $process_note_content_extra_args
*/
public function onProcessNoteContent(Note $note, string $content, string $content_type, array $process_note_content_extra_args = []): EventResult
{
@ -150,7 +152,12 @@ class Link extends Component
public const URL_SCHEME_NO_DOMAIN = 4;
public const URL_SCHEME_COLON_COORDINATES = 8;
public function URLSchemes($filter = null)
/**
* @param self::URL_SCHEME_COLON_COORDINATES|self::URL_SCHEME_COLON_DOUBLE_SLASH|self::URL_SCHEME_NO_DOMAIN|self::URL_SCHEME_SINGLE_COLON $filter
*
* @return string[]
*/
public function URLSchemes(?int $filter = null): array
{
// TODO: move these to config
$schemes = [
@ -197,6 +204,7 @@ class Link extends Component
* Intermediate callback for `replaceURLs()`, which helps resolve some
* ambiguous link forms before passing on to the final callback.
*
* @param string[] $matches
* @param callable(string $text): string $callback: return replacement text
*/
private function callbackHelper(array $matches, callable $callback): string

@ -44,6 +44,8 @@ class Feed extends Controller
{
/**
* Everything with attention to current user
*
* @return ControllerResultType
*/
public function notifications(Request $request): array
{

@ -117,7 +117,7 @@ class Notification extends Entity
/**
* Pull the complete list of known activity context notifications for this activity.
*
* @return array of integer actor ids (also group profiles)
* @return int[] actor ids (also group profiles)
*/
public static function getNotificationTargetIdsByActivity(int|Activity $activity_id): array
{
@ -129,11 +129,17 @@ class Notification extends Entity
return $targets;
}
/**
* @return int[]
*/
public function getNotificationTargetsByActivity(int|Activity $activity_id): array
{
return DB::findBy(Actor::class, ['id' => $this->getNotificationTargetIdsByActivity($activity_id)]);
}
/**
* @return int[]
*/
public static function getAllActivitiesTargetedAtActor(Actor $actor): array
{
return DB::dql(<<<'EOF'

@ -71,10 +71,10 @@ class Notification extends Component
* Example of $targets:
* [42, $actor_alice, $actor_bob] // Avoid repeating actors or ids
*
* @param Actor $sender The one responsible for this activity, take care not to include it in targets
* @param Activity $activity The activity responsible for the object being given to known to targets
* @param array $targets Attentions, Mentions, any other source. Should never be empty, you usually want to register an attention to every $sender->getSubscribers()
* @param null|string $reason An optional reason explaining why this notification exists
* @param Actor $sender The one responsible for this activity, take care not to include it in targets
* @param Activity $activity The activity responsible for the object being given to known to targets
* @param non-empty-array<Actor|int> $targets Attentions, Mentions, any other source. Should never be empty, you usually want to register an attention to every $sender->getSubscribers()
* @param null|string $reason An optional reason explaining why this notification exists
*/
public function onNewNotification(Actor $sender, Activity $activity, array $targets, ?string $reason = null): EventResult
{
@ -103,12 +103,19 @@ class Notification extends Component
return Event::next;
}
/**
* @param mixed[] $retry_args
*/
public function onQueueNotificationLocal(Actor $sender, Activity $activity, Actor $target, ?string $reason, array &$retry_args): EventResult
{
// TODO: use https://symfony.com/doc/current/notifier.html
return Event::stop;
}
/**
* @param Actor[] $targets
* @param mixed[] $retry_args
*/
public function onQueueNotificationRemote(Actor $sender, Activity $activity, array $targets, ?string $reason, array &$retry_args): EventResult
{
if (FreeNetwork::notify($sender, $activity, $targets, $reason)) {
@ -122,7 +129,9 @@ class Notification extends Component
* Bring given Activity to Targets' knowledge.
* This will flush a Notification to DB.
*
* @return true if successful, false otherwise
* @param Actor[] $targets
*
* @return bool true if successful, false otherwise
*/
public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool
{
@ -134,7 +143,7 @@ class Notification extends Component
continue;
}
if (Event::handle('NewNotificationShould', [$activity, $target]) === Event::next) {
if ($sender->getId() === $target->getId()
if ($sender->getId() === $target->getId()
|| $activity->getActorId() === $target->getId()) {
// The target already knows about this, no need to bother with a notification
continue;

@ -34,11 +34,16 @@ use App\Util\HTML\Heading;
use Component\Collection\Util\Controller\FeedController;
use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class PersonFeed extends FeedController
{
/**
* @throws ClientException
* @throws ServerException
*
* @return ControllerResultType
*/
public function personViewId(Request $request, int $id): array
{
@ -62,6 +67,8 @@ class PersonFeed extends FeedController
*
* @throws ClientException
* @throws ServerException
*
* @return ControllerResultType
*/
public function personViewNickname(Request $request, string $nickname): array
{
@ -73,6 +80,9 @@ class PersonFeed extends FeedController
return $this->personView($request, $person);
}
/**
* @return ControllerResultType
*/
public function personView(Request $request, Actor $person): array
{
return [

@ -88,6 +88,8 @@ class PersonSettings extends Controller
* @throws NoLoggedInUser
* @throws RedirectException
* @throws ServerException
*
* @return ControllerResultType
*/
public function allSettings(Request $request, LanguageController $language): array
{
@ -205,6 +207,8 @@ class PersonSettings extends Controller
* @throws \Doctrine\DBAL\Exception
* @throws NoLoggedInUser
* @throws ServerException
*
* @return ControllerResultType[]
*/
private static function notifications(Request $request): array
{
@ -251,7 +255,7 @@ class PersonSettings extends Controller
// @codeCoverageIgnoreStart
Log::critical("Structure of table user_notification_prefs changed in a way not accounted to in notification settings ({$name}): " . $type_str);
throw new ServerException(_m('Internal server error'));
// @codeCoverageIgnoreEnd
// @codeCoverageIgnoreEnd
}
}

@ -44,11 +44,13 @@ use App\Util\Exception\ServerException;
use App\Util\Formatting;
use App\Util\HTML;
use Component\Attachment\Entity\ActorToAttachment;
use Component\Attachment\Entity\Attachment;
use Component\Attachment\Entity\AttachmentToNote;
use Component\Conversation\Conversation;
use Component\Language\Entity\Language;
use Component\Notification\Entity\Attention;
use EventResult;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
@ -66,6 +68,8 @@ class Posting extends Component
* HTML render event handler responsible for adding and handling
* the result of adding the note submission form, only if a user is logged in
*
* @param array{post_form?: FormInterface} $res
*
* @throws BugFoundException
* @throws ClientException
* @throws DuplicateFoundException
@ -84,9 +88,25 @@ class Posting extends Component
}
/**
* @param Actor $actor The Actor responsible for the creation of this Note
* @param null|string $content The raw text content
* @param string $content_type Indicating one of the various supported content format (Plain Text, Markdown, LaTeX...)
* @param null|string $locale Note's written text language, set by the default Actor language or upon filling
* @param null|VisibilityScope $scope The visibility of this Note
* @param Actor[]|int[] $attentions Actor|int[]: In Group/To Person or Bot, registers an attention between note and target
* @param null|int|Note $reply_to The soon-to-be Note parent's id, if it's a Reply itself
* @param UploadedFile[] $attachments UploadedFile[] to be stored as GSFiles associated to this note
* @param array<array{Attachment, string}> $processed_attachments Array of [Attachment, Attachment's name][] to be associated to this $actor and Note
* @param array{note?: Note, content?: string, content_type?: string, extra_args?: array<string, mixed>} $process_note_content_extra_args Extra arguments for the event ProcessNoteContent
* @param bool $flush_and_notify True if the newly created Note activity should be passed on as a Notification
* @param null|string $rendered The Note's content post RenderNoteContent event, which sanitizes and processes the raw content sent
* @param string $source The source of this Note
*
* @throws ClientException
* @throws DuplicateFoundException
* @throws ServerException
*
* @return array{\App\Entity\Activity, \App\Entity\Note, array<int, \App\Entity\Actor>}
*/
public static function storeLocalArticle(
Actor $actor,
@ -144,25 +164,25 @@ class Posting extends Component
* $actor_id, possibly as a reply to note $reply_to and with flag
* $is_local. Sanitizes $content and $attachments
*
* @param Actor $actor The Actor responsible for the creation of this Note
* @param null|string $content The raw text content
* @param string $content_type Indicating one of the various supported content format (Plain Text, Markdown, LaTeX...)
* @param null|string $locale Note's written text language, set by the default Actor language or upon filling
* @param null|VisibilityScope $scope The visibility of this Note
* @param array $attentions Actor|int[]: In Group/To Person or Bot, registers an attention between note and target
* @param null|int|Note $reply_to The soon-to-be Note parent's id, if it's a Reply itself
* @param array $attachments UploadedFile[] to be stored as GSFiles associated to this note
* @param array $processed_attachments Array of [Attachment, Attachment's name][] to be associated to this $actor and Note
* @param array $process_note_content_extra_args Extra arguments for the event ProcessNoteContent
* @param bool $flush_and_notify True if the newly created Note activity should be passed on as a Notification
* @param null|string $rendered The Note's content post RenderNoteContent event, which sanitizes and processes the raw content sent
* @param string $source The source of this Note
* @param Actor $actor The Actor responsible for the creation of this Note
* @param null|string $content The raw text content
* @param string $content_type Indicating one of the various supported content format (Plain Text, Markdown, LaTeX...)
* @param null|string $locale Note's written text language, set by the default Actor language or upon filling
* @param null|VisibilityScope $scope The visibility of this Note
* @param Actor[]|int[] $attentions Actor|int[]: In Group/To Person or Bot, registers an attention between note and targte
* @param null|int|Note $reply_to The soon-to-be Note parent's id, if it's a Reply itself
* @param UploadedFile[] $attachments UploadedFile[] to be stored as GSFiles associated to this note
* @param array<array{Attachment, string}> $processed_attachments Array of [Attachment, Attachment's name][] to be associated to this $actor and Note
* @param array{note?: Note, content?: string, content_type?: string, extra_args?: array<string, mixed>} $process_note_content_extra_args Extra arguments for the event ProcessNoteContent
* @param bool $flush_and_notify True if the newly created Note activity should be passed on as a Notification
* @param null|string $rendered The Note's content post RenderNoteContent event, which sanitizes and processes the raw content sent
* @param string $source The source of this Note
*
* @throws ClientException
* @throws DuplicateFoundException
* @throws ServerException
*
* @return array [Activity, Note, Effective Attentions]
* @return array{\App\Entity\Activity, \App\Entity\Note, array<int, \App\Entity\Actor>}
*/
public static function storeLocalNote(
Actor $actor,
@ -302,6 +322,9 @@ class Posting extends Component
return [$activity, $note, $effective_attentions];
}
/**
* @param array<int, \App\Entity\Actor> $mentions
*/
public function onRenderNoteContent(string $content, string $content_type, ?string &$rendered, Actor $author, ?string $language = null, array &$mentions = []): EventResult
{
switch ($content_type) {

@ -38,12 +38,17 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class Search extends FeedController
{
/**
* Handle a search query
*
* @return ControllerResultType
*/
public function handle(Request $request)
public function handle(Request $request): array
{
$actor = Common::actor();
$language = !\is_null($actor) ? $actor->getTopLanguage()->getLocale() : null;

@ -134,6 +134,8 @@ class Search extends Component
/**
* Add the search form to the site header
*
* @param string[] $elements
*
* @throws RedirectException
*/
public function onPrependRightPanelBlock(Request $request, array &$elements): EventResult
@ -145,7 +147,7 @@ class Search extends Component
/**
* Output our dedicated stylesheet
*
* @param array $styles stylesheets path
* @param string[] $styles stylesheets path
*/
public function onEndShowStyles(array &$styles, string $route): EventResult
{

@ -45,6 +45,8 @@ class Subscribers extends CircleController
{
/**
* @throws ServerException
*
* @return ControllerResultType
*/
public function subscribersByActor(Request $request, Actor $actor): array
{
@ -61,6 +63,8 @@ class Subscribers extends CircleController
/**
* @throws ClientException
* @throws ServerException
*
* @return ControllerResultType
*/
public function subscribersByActorId(Request $request, int $id): array
{
@ -78,6 +82,8 @@ class Subscribers extends CircleController
* @throws \App\Util\Exception\ServerException
* @throws ClientException
* @throws RedirectException
*
* @return ControllerResultType
*/
public function subscribersAdd(Request $request, int $object_id): array
{
@ -126,6 +132,8 @@ class Subscribers extends CircleController
* @throws \App\Util\Exception\ServerException
* @throws ClientException
* @throws RedirectException
*
* @return ControllerResultType
*/
public function subscribersRemove(Request $request, int $object_id): array
{

@ -38,6 +38,8 @@ class Subscriptions extends CircleController
/**
* @throws ClientException
* @throws ServerException
*
* @return ControllerResultType
*/
public function subscriptionsByActorId(Request $request, int $id): array
{
@ -48,7 +50,10 @@ class Subscriptions extends CircleController
return $this->subscriptionsByActor($request, $actor);
}
public function subscriptionsByActor(Request $request, Actor $actor)
/**
* @return ControllerResultType
*/
public function subscriptionsByActor(Request $request, Actor $actor): array
{
return [
'_template' => 'collection/actors.html.twig',

@ -58,6 +58,8 @@ class Subscription extends Component
*
* @param Actor|int|LocalUser $subject The Actor who subscribed or unsubscribed
* @param Actor|int|LocalUser $object The Actor who was subscribed or unsubscribed from
*
* @return array{bool, bool}
*/
public static function refreshSubscriptionCount(int|Actor|LocalUser $subject, int|Actor|LocalUser $object): array
{
@ -177,9 +179,10 @@ class Subscription extends Component
* In the case of ``\App\Component\Subscription``, the action added allows a **LocalUser** to **subscribe** or
* **unsubscribe** a given **Actor**.
*
* @param Actor $object The Actor on which the action is to be performed
* @param array $actions An array containing all actions added to the
* current profile, this event adds an action to it
* @param Actor $object The Actor on which the action is to be performed
* @param array<array{url: string, title: string, classes: string, id: string}> $actions
* An array containing all actions added to the
* current profile, this event adds an action to it
*
* @throws DuplicateFoundException
* @throws NotFoundException

@ -15,7 +15,12 @@ class Tag extends Controller
// TODO: Use Feed::query
// TODO: If ?canonical=something, respect
// TODO: Allow to set locale of tag being selected
private function process(null|string|array $tag_single_or_multi, string $key, string $query, string $template, bool $include_locale = false)
/**
* @param (null|string|string[]) $tag_single_or_multi
*
* @return ControllerResultType
*/
private function process(null|string|array $tag_single_or_multi, string $key, string $query, string $template, bool $include_locale = false): array
{
$actor = Common::actor();
$page = $this->int('page') ?: 1;
@ -46,7 +51,10 @@ class Tag extends Controller
];
}
public function single_note_tag(string $tag)
/**
* @return ControllerResultType
*/
public function single_note_tag(string $tag): array
{
return $this->process(
tag_single_or_multi: $tag,
@ -57,7 +65,10 @@ class Tag extends Controller
);
}
public function multi_note_tags(string $tags)
/**
* @return ControllerResultType
*/
public function multi_note_tags(string $tags): array
{
return $this->process(
tag_single_or_multi: explode(',', $tags),

@ -134,6 +134,9 @@ class NoteTag extends Entity
return "note-tags-{$note_id}";
}
/**
* @return NoteTag[]
*/
public static function getByNoteId(int $note_id): array
{
return Cache::getList(self::cacheKey($note_id), fn () => DB::dql('SELECT nt FROM note_tag AS nt JOIN note AS n WITH n.id = nt.note_id WHERE n.id = :id', ['id' => $note_id]));

@ -119,6 +119,8 @@ class NoteTagBlock extends Entity
/**
* Check whether $note_tag is considered blocked by one of
* $note_tag_blocks
*
* @param NoteTagBlock[] $note_tag_blocks
*/
public static function checkBlocksNoteTag(NoteTag $note_tag, array $note_tag_blocks): bool
{

@ -61,13 +61,16 @@ class Tag extends Component
public const TAG_REGEX = '/(^|\\s)(#[\\pL\\pN_\\-]{1,64})/u'; // Brion Vibber 2011-02-23 v2:classes/Notice.php:367 function saveTags
public const TAG_SLUG_REGEX = '[A-Za-z0-9]{1,64}';
public function onAddRoute($r): EventResult
public function onAddRoute(Router $r): EventResult
{
$r->connect('single_note_tag', '/note-tag/{tag<' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'single_note_tag']);
$r->connect('multi_note_tags', '/note-tags/{tags<(' . self::TAG_SLUG_REGEX . ',)+' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'multi_note_tags']);
return Event::next;
}
/**
* @param array{tag_use_canonical?: bool} $extra_args
*/
public static function maybeCreateTag(string $tag, int $note_id, ?int $lang_id, array $extra_args = []): ?NoteTag
{
if (!self::validate($tag)) {
@ -118,6 +121,8 @@ class Tag extends Component
/**
* Process note by extracting any tags present
*
* @param array{TagProcessed?: bool} $extra_args
*/
public function onProcessNoteContent(Note $note, string $content, string $content_type, array $extra_args): EventResult
{
@ -213,6 +218,9 @@ class Tag extends Component
* Populate $note_expr with an expression to match a tag, if the term looks like a tag
*
* $term /^(note|tag|people|actor)/ means we want to match only either a note or an actor
*
* @param mixed $note_expr
* @param mixed $actor_expr
*/
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
{
@ -264,12 +272,19 @@ class Tag extends Component
return Event::next;
}