Bring in magazine_slug multi-tenancy, article_magazine links, and controller
fixes from imwald. GitCitadel hub compose drops its bundled MySQL and joins
the imwald stack network (unfold_default) with DATABASE_HOST=unfold-mysql.
Co-authored-by: Cursor <cursoragent@cursor.com>
- compose.hub.yaml: project name → gitcitadel, default HTTP_PUBLISH → 127.0.0.1:9085
(matches Apache ProxyPass to http://127.0.0.1:9085/ on gitcitadel.imwald.eu)
- .env.dist: HTTP_PORT=9085, HTTP_PUBLISH=127.0.0.1:9085 (uncommented)
- config/unfold.yaml: site name/description/theme/relays/nip05_domain updated for
GitCitadel; npub and d_tag_magazine marked TODO until the org values are known
Co-authored-by: Cursor <cursoragent@cursor.com>
| Published articles (30023/24) | **MySQL**`article` table (global rows) + `article_magazine` (which magazine tenant ingested/references each row) |
| Magazine index (30040), kind-0 **profiles**, NIP-65 **relay lists** (10002) | **MySQL**`event` table with stable `core_row_key`(filled by `app:prewarm` and on-demand fetches) |
| Magazine index (30040), kind-0 **profiles**, NIP-65 **relay lists** (10002) | **MySQL**`event` table with stable `core_row_key`— magazine indices are prefixed with `magazine_slug`; profiles/relay lists are shared |
| Comment / reply / thread **UI** (fetched thread HTML, etc.) | **Filesystem cache** pool `cache.replies` (not the DB) |
| Comment / reply / thread **UI** (fetched thread HTML, etc.) | **Filesystem cache** pool `cache.replies` (not the DB) |
| Generic Symfony `cache.app` | Other app caches; **not** used for long-term profile or magazine index storage |
| Generic Symfony `cache.app` | Other app caches; **not** used for long-term profile or magazine index storage |
@ -115,7 +115,7 @@ For a full **Nostr backfill** + one-shot prewarm, use **`make prewarm`** (or a h
| What | File |
| What | File |
|------|------|
|------|------|
| Site title, `npub`, `d_tag`, **relays** (`default_relay`, `article_relays`, `profile_relays`), theme | `config/unfold.yaml` (imported as Symfony parameters) |
| Site title, `npub`, `d_tag`, **`magazine_slug`** (tenant id for shared MySQL), **relays** (`default_relay`, `article_relays`, `profile_relays`), theme | `config/unfold.yaml` (imported as Symfony parameters) |
| `MAGAZINE_PREWARM_PREFER_SLUGS` | `.env` / `.env.local` — optional comma-separated category slugs to prioritize in `app:prewarm` magazine phase (after the root). Use when the relay time budget would otherwise skip your updated category. |
| `MAGAZINE_PREWARM_PREFER_SLUGS` | `.env` / `.env.local` — optional comma-separated category slugs to prioritize in `app:prewarm` magazine phase (after the root). Use when the relay time budget would otherwise skip your updated category. |
| `DATABASE_URL`, `APP_SECRET`, `HTTP_PORT`, `MYSQL_*`, optional **`PREWARM_FLAGS`** (for the Docker `cron` service) | `.env` / `.env.local` (see `.env.dist`) |
| `DATABASE_URL`, `APP_SECRET`, `HTTP_PORT`, `MYSQL_*`, optional **`PREWARM_FLAGS`** (for the Docker `cron` service) | `.env` / `.env.local` (see `.env.dist`) |
| Cache pool definitions (`cache.replies`, `cache.drafts`, `cache.app`) | `config/packages/cache.yaml` |
| Cache pool definitions (`cache.replies`, `cache.drafts`, `cache.app`) | `config/packages/cache.yaml` |
@ -123,37 +123,46 @@ For a full **Nostr backfill** + one-shot prewarm, use **`make prewarm`** (or a h
**Relays (short):** `default_relay` and `article_relays` drive article sync and many queries; `profile_relays` are used **first** for kind-0 / profile fetches, then the merged default + article set (see `NostrClient`).
**Relays (short):** `default_relay` and `article_relays` drive article sync and many queries; `profile_relays` are used **first** for kind-0 / profile fetches, then the merged default + article set (see `NostrClient`).
### Shared MySQL (multiple magazines on one host)
Two deployments (e.g. Imwald + GitCitadel) can use **one MySQL** instead of separate `database_data` volumes:
1. Set a unique **`magazine_slug`** in each image’s `config/unfold.yaml` (e.g. `imwald`, `gitcitadel`).
2. Run **one** MySQL container (or external server). Point both stacks at the same **`DATABASE_URL`** (host port or Docker network alias).
3. **Nuke old volumes** and run migrations once: `docker compose exec php php bin/console doctrine:migrations:migrate --no-interaction`.
4. Backfill each site separately (`articles:get`, `app:prewarm`) — each container tags rows with its own `magazine_slug`.
Articles and kind-0 profiles are stored once and shared; magazine indices, featured authors, admin users, and list/search/sitemap views are scoped per `magazine_slug`.
---
---
## Production / Hub (remote server)
## Production / Hub (remote server)
The app runs as a **pre-built** image (no app source on the server). The server only needs `compose.hub.yaml`, a `.env`, and Docker. Default image: `silberengel/unfold:latest`; override with **`UNFOLD_DOCKER_IMAGE`**.
The app runs as a **pre-built** image (no app source on the server). The server only needs `compose.hub.yaml`, a `.env`, and Docker. Default image: **`silberengel/unfold:gitcitadel`** (GitCitadel site on the **`gitcitadel`** branch); override with **`UNFOLD_DOCKER_IMAGE`**.
| Topic | Notes |
| Topic | Notes |
|-------|--------|
|-------|--------|
| `compose.hub.yaml` | Defines **`php`** (FrankenPHP) + **`database`** (MySQL) + **`prewarm`** (same app image: **`app:prewarm` every 10 minutes**, like dev’s `docker/cron`). Optional: disable `prewarm` in Compose if you prefer a host `cron` only. |
| `compose.hub.yaml` | Compose project **`gitcitadel`** (containers e.g. `gitcitadel-php-1`). **`php`** + **`prewarm`** only — **no bundled MySQL**; connects to the imwald hub DB (`unfold-mysql` on network `unfold_default`). Start the **imwald** hub stack first. |
| HTTP | **`HTTP_PUBLISH`** in `.env` maps **host** port → container **80** (default **9080**). Put a reverse proxy (e.g. Apache) in front; set **`TRUSTED_PROXIES`** to match your proxy (often include `127.0.0.0/8` and the Docker bridge CIDR, e.g. `172.16.0.0/12`). |
| HTTP | **`HTTP_PUBLISH`** in `.env` maps **host** port → container **80** (default **`127.0.0.1:9085`** for `gitcitadel.imwald.eu`). Put a reverse proxy (e.g. Apache) in front; set **`TRUSTED_PROXIES`** to match your proxy (often include `127.0.0.0/8` and the Docker bridge CIDR, e.g. `172.16.0.0/12`). |
| Secrets | Real **`APP_SECRET`** and **`MYSQL_*`** (or external DB via `DATABASE_URL` if you change the file). Do not commit production `.env`. |
| Secrets | Real **`APP_SECRET`** and **`MYSQL_*`** must match the **imwald** hub stack (same DB user/database). Optional **`DATABASE_HOST`** (default `unfold-mysql`). Do not commit production `.env`. |
| `PREWARM_FLAGS` | Optional extra CLI args for the hub **`prewarm`** service (and dev **`cron`**). After editing `.env`, run `docker compose -f compose.hub.yaml up -d --force-recreate prewarm`. |
| `PREWARM_FLAGS` | Optional extra CLI args for the hub **`prewarm`** service (and dev **`cron`**). After editing `.env`, run `docker compose -f compose.hub.yaml up -d --force-recreate prewarm`. |
### Build, tag, and push (on your machine or CI)
### Build, tag, and push (on your machine or CI)
From the **repository root** (same `Dockerfile` as local prod):
From the **repository root**on the **`gitcitadel`** branch (same `Dockerfile` as local prod):
```bash
```bash
# Production image
# GitCitadel hub image (default in compose.hub.yaml)
- Use **`linux/amd64`** if the server is amd64; use **`arm64`** (or a matching `--platform`) for arm servers.
- Use **`linux/amd64`** if the server is amd64; use **`arm64`** (or a matching `--platform`) for arm servers.
- The image name must match what the server will pull: either keep**`UNFOLD_DOCKER_IMAGE=YOUR_REGISTRY/unfold:TAG`** in server `.env`, or push to the default name **`silberengel/unfold:latest`**.
- The image tag must match what the server pulls: push to **`silberengel/unfold:gitcitadel`**, or set**`UNFOLD_DOCKER_IMAGE=YOUR_REGISTRY/unfold:TAG`** in server `.env`.
### Deploy on the server (pull, up, migrate)
### Deploy on the server (pull, up, migrate)
@ -185,7 +194,7 @@ make -f Makefile.hub backfill # up + migrate + articles-get + prewarm-onc
# Per-relay WebSocket I/O (seconds) in NostrClient; also default_socket_timeout during app:prewarm.
# Per-relay WebSocket I/O (seconds) in NostrClient; also default_socket_timeout during app:prewarm.
nostr_relay_request_timeout_sec:12
nostr_relay_request_timeout_sec:12
name:'Nostr, Curated Thoughtfully'
# Stable tenant id for shared MySQL (magazine indices, article visibility, featured authors, admin users).
short_name:'Imwald Blog'
# Lowercase alnum and hyphens only; must be unique per deployment on the same database.
description:'A selection of my own Nostr long-form articles and articles from other authors, selected for the quality of their writing and the depth of their analysis.'
magazine_slug:'gitcitadel'
og_headline:'Nostr, Curated Thoughtfully'
name:'GitCitadel Homepage'
og_subheading:'Imwald Blog by Laeserin'
short_name:'GitCitadel Homepage'
description:'GitCitadel — Nostr-native open-source software development tools and infrastructure.'
default_relay:'wss://theforest.nostr1.com'
og_headline:'GitCitadel Homepage'
og_subheading:'Nostr-native publishing and development tools'
default_relay:'wss://thecitadel.nostr1.com'
# Extra wss:// URLs for article sync (articles:get), comment threads (NIP-22 / getArticleDiscussion),
# Extra wss:// URLs for article sync (articles:get), comment threads (NIP-22 / getArticleDiscussion),
# and any request that merges the default set with author-specific relays. default_relay is first; duplicates ignored.
# and any request that merges the default set with author-specific relays. default_relay is first; duplicates ignored.
* Multi-tenant shared MySQL: magazine_slug scopes site data; article rows stay global with article_magazine links.
*/
final class Version20260528140000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Shared DB multi-tenancy: article_magazine, magazine_slug on featured_author and app_user';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE article_magazine (magazine_slug VARCHAR(64) NOT NULL, article_id INT NOT NULL, INDEX IDX_ARTICLE_MAGAZINE_ARTICLE (article_id), PRIMARY KEY (magazine_slug, article_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
message: '#^Call to function is_array\(\) with bool\|int\|string\|null will always evaluate to false\.$#'
identifier: function.impossibleType
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 2
path: src/Command/PrewarmCommand.php
-
message: '#^Call to function method_exists\(\) with Symfony\\Component\\Console\\Helper\\ProgressBar and ''clear'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Call to function method_exists\(\) with Symfony\\Component\\Console\\Helper\\ProgressBar and ''setMinSecondsBetwee…'' will always evaluate to false\.$#'
identifier: function.impossibleType
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''categories'' on array\{categories\: int, listed\: int, resolved\: int, missing\: int\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''categories'' on array\{categories\: list\<array\{slug\: string, title\: string, event_id\: string, listed_total\: int, resolved_total\: int, missing_total\: int, entries\: list\<array\{coordinate\: string, status\: string, reason\: string, article_title\?\: string\}\>\}\>, totals\: array\{categories\: int, listed\: int, resolved\: int, missing\: int\}\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''coordinate'' on array\{coordinate\: string, status\: string, reason\: string, article_title\?\: string\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''entries'' on array\{slug\: string, title\: string, event_id\: string, listed_total\: int, resolved_total\: int, missing_total\: int, entries\: list\<array\{coordinate\: string, status\: string, reason\: string, article_title\?\: string\}\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''event_id'' on array\{slug\: string, title\: string, event_id\: string, listed_total\: int, resolved_total\: int, missing_total\: int, entries\: list\<array\{coordinate\: string, status\: string, reason\: string, article_title\?\: string\}\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''kind0_tags'' on array\{content\: stdClass, kind0_tags\: list\<list\<string\>\>, nip30_custom_emojis\: list\<array\{shortcode\: string, url\: string, set\?\: string\}\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''label'' on array\{label\: string, href\: string\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''listed'' on array\{categories\: int, listed\: int, resolved\: int, missing\: int\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''listed_total'' on array\{slug\: string, title\: string, event_id\: string, listed_total\: int, resolved_total\: int, missing_total\: int, entries\: list\<array\{coordinate\: string, status\: string, reason\: string, article_title\?\: string\}\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''missing'' on array\{categories\: int, listed\: int, resolved\: int, missing\: int\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''missing_total'' on array\{slug\: string, title\: string, event_id\: string, listed_total\: int, resolved_total\: int, missing_total\: int, entries\: list\<array\{coordinate\: string, status\: string, reason\: string, article_title\?\: string\}\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''reason'' on array\{coordinate\: string, status\: string, reason\: string, article_title\?\: string\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''resolved'' on array\{categories\: int, listed\: int, resolved\: int, missing\: int\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''resolved_total'' on array\{slug\: string, title\: string, event_id\: string, listed_total\: int, resolved_total\: int, missing_total\: int, entries\: list\<array\{coordinate\: string, status\: string, reason\: string, article_title\?\: string\}\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''slug'' on array\{slug\: string, title\: string, event_id\: string, listed_total\: int, resolved_total\: int, missing_total\: int, entries\: list\<array\{coordinate\: string, status\: string, reason\: string, article_title\?\: string\}\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''status'' on array\{coordinate\: string, status\: string, reason\: string, article_title\?\: string\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''title'' on array\{slug\: string, title\: string, event_id\: string, listed_total\: int, resolved_total\: int, missing_total\: int, entries\: list\<array\{coordinate\: string, status\: string, reason\: string, article_title\?\: string\}\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset ''totals'' on array\{categories\: list\<array\{slug\: string, title\: string, event_id\: string, listed_total\: int, resolved_total\: int, missing_total\: int, entries\: list\<array\{coordinate\: string, status\: string, reason\: string, article_title\?\: string\}\>\}\>, totals\: array\{categories\: int, listed\: int, resolved\: int, missing\: int\}\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Offset 0 on non\-empty\-list\<non\-falsy\-string\> on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Strict comparison using \=\=\= between \*NEVER\* and 1 will always evaluate to false\.$#'
identifier: identical.alwaysFalse
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Strict comparison using \=\=\= between array\{\} and array\{\} will always evaluate to true\.$#'
identifier: identical.alwaysTrue
count: 1
path: src/Command/PrewarmCommand.php
-
message: '#^Using nullsafe property access "\?\-\>value" on left side of \?\? is unnecessary\. Use \-\> instead\.$#'
identifier: nullsafe.neverNull
count: 1
path: src/Command/PrewarmCommand.php
-
-
message: '#^Call to an undefined method Symfony\\Component\\Form\\FormInterface\<mixed\>\:\:getClickedButton\(\)\.$#'
message: '#^Call to an undefined method Symfony\\Component\\Form\\FormInterface\<mixed\>\:\:getClickedButton\(\)\.$#'
identifier: method.notFound
identifier: method.notFound
@ -193,280 +25,10 @@ parameters:
path: src/Controller/ArticleController.php
path: src/Controller/ArticleController.php
-
-
message: '#^Call to function is_object\(\) with object will always evaluate to true\.$#'
message: '#^PHPDoc tag @param references unknown parameter\: \$rawTags$#'
identifier: function.alreadyNarrowedType
identifier: parameter.notFound
count: 1
path: src/Controller/ArticleController.php
-
message: '#^Call to function is_object\(\) with stdClass will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Controller/ArticleController.php
-
message: '#^Offset ''comment_reply…'' on array\{list\: array\<int, object\>, quotes\: array\<int, object\>, commentLinks\: array\<string, array\<int, mixed\>\>, quoteLinks\: array\<string, array\<int, mixed\>\>, processedContent\: array\<string, string\>, comment_reply_context\: array\{can_publish\: bool, coordinate\: string, article_event_id\: string\|null, parent_kind\: int, rows\: array\<int, array\<string, mixed\>\>, fragment_url\: string\}\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Controller/ArticleController.php
-
message: '#^Offset ''list'' on array\{list\: array\<int, object\>, quotes\: array\<int, object\>, commentLinks\: array\<string, array\<int, mixed\>\>, quoteLinks\: array\<string, array\<int, mixed\>\>, processedContent\: array\<string, string\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Controller/ArticleController.php
-
message: '#^Offset 0 on non\-empty\-list\<string\> in isset\(\) always exists and is not nullable\.$#'
identifier: isset.offset
count: 1
path: src/Controller/ArticleController.php
-
message: '#^Using nullsafe property access "\?\-\>value" on left side of \?\? is unnecessary\. Use \-\> instead\.$#'
identifier: nullsafe.neverNull
count: 1
path: src/Controller/ArticleController.php
-
message: '#^Offset ''ok_relays'' on array\{ok\: true, id\: string, relays\: array\<string, mixed\>, ok_relays\: int, total_relays\: int\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Controller/CommentReplyController.php
-
message: '#^Offset ''total_relays'' on array\{ok\: true, id\: string, relays\: array\<string, mixed\>, ok_relays\: int, total_relays\: int\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Controller/CommentReplyController.php
-
message: '#^Negated boolean expression is always false\.$#'
identifier: booleanNot.alwaysFalse
count: 1
path: src/Controller/DefaultController.php
-
message: '#^Call to function is_array\(\) with array\<int, string\> will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Controller/SeoController.php
-
message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Controller/SeoController.php
-
message: '#^Offset ''list'' on array\{list\: list\<App\\Entity\\Article\>, category\: array\{title\: string, summary\: string\}, pagination\: array\{page\: int, per_page\: int, total\: int, last_page\: int\}\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Controller/SeoController.php
-
message: '#^Offset ''summary'' on array\{title\: string, summary\: string\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Controller/SeoController.php
-
message: '#^Offset ''title'' on array\{title\: string, summary\: string\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Controller/SeoController.php
-
message: '#^Call to function is_array\(\) with non\-empty\-array will always evaluate to true\.$#'
message: '#^Call to function is_object\(\) with object will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 2
path: src/Service/ArticleCommentThreadLoader.php
-
message: '#^Offset ''partial'' on array\{thread\: array\<int, object\>, quotes\: array\<int, object\>\} on left side of \?\? does not exist\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/ArticleCommentThreadLoader.php
-
message: '#^Offset ''quotes'' on array\{thread\: array\<int, object\>, quotes\: array\<int, object\>, partial\?\: bool\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/ArticleCommentThreadLoader.php
-
message: '#^Offset ''quotes'' on array\{thread\: array\<int, object\>, quotes\: array\<int, object\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/ArticleCommentThreadLoader.php
-
message: '#^Offset ''thread'' on array\{thread\: array\<int, object\>, quotes\: array\<int, object\>, partial\?\: bool\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/ArticleCommentThreadLoader.php
-
message: '#^Offset ''thread'' on array\{thread\: array\<int, object\>, quotes\: array\<int, object\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/ArticleCommentThreadLoader.php
-
message: '#^Offset 0 on non\-empty\-list\<string\> on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/ArticleCommentThreadLoader.php
-
message: '#^Call to function is_object\(\) with stdClass will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 2
path: src/Service/CacheService.php
-
message: '#^Negated boolean expression is always false\.$#'
identifier: booleanNot.alwaysFalse
count: 1
path: src/Service/CacheService.php
-
message: '#^Offset 0 on non\-empty\-list\<string\> on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/CacheService.php
-
message: '#^Parameter \#1 \$array \(non\-empty\-list\<string\>\) of array_values is already a list, call has no effect\.$#'
identifier: arrayValues.list
count: 1
path: src/Service/CacheService.php
-
message: '#^Strict comparison using \!\=\= between non\-empty\-list\<string\> and array\{\} will always evaluate to true\.$#'
identifier: notIdentical.alwaysTrue
count: 1
path: src/Service/CacheService.php
-
message: '#^Comparison operation "\>\=" between 3 and 2 is always true\.$#'
identifier: greaterOrEqual.alwaysTrue
count: 1
path: src/Service/CommentReplyService.php
-
message: '#^Call to function is_object\(\) with object will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Service/HighlightSyncService.php
-
message: '#^Using nullsafe property access "\?\-\>value" on left side of \?\? is unnecessary\. Use \-\> instead\.$#'
identifier: nullsafe.neverNull
count: 1
path: src/Service/HighlightSyncService.php
-
message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 3
path: src/Service/MagazineContentService.php
-
message: '#^Offset ''categories'' on array\{categories\: list\<array\{entries\: list\<array\{coordinate\: string, status\: string, reason\: string\}\>\}\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/MagazineContentService.php
-
message: '#^Offset ''coordinate'' on array\{coordinate\: string, status\: ''missing'', reason\: ''article_not_in_db''\} in isset\(\) always exists and is not nullable\.$#'
identifier: isset.offset
count: 1
path: src/Service/MagazineContentService.php
-
message: '#^Offset ''entries'' on array\{entries\: list\<array\{coordinate\: string, status\: string, reason\: string\}\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/MagazineContentService.php
-
message: '#^Offset ''reason'' on array\{coordinate\: string, status\: ''missing'', reason\: string\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/MagazineContentService.php
-
message: '#^Offset ''status'' on array\{coordinate\: string, status\: string, reason\: string\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/MagazineContentService.php
-
message: '#^Offset 0 on non\-empty\-list\<string\> on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
count: 1
path: src/Service/MagazineContentService.php
path: src/Nostr/Nip10Kind1ArticleReplyTags.php
-
-
message: '#^Cannot call method __invoke\(\) on callable\.$#'
message: '#^Cannot call method __invoke\(\) on callable\.$#'
@ -474,194 +36,26 @@ parameters:
count: 4
count: 4
path: src/Service/MagazineRefresher.php
path: src/Service/MagazineRefresher.php
-
message: '#^Offset ''label'' on array\{label\: string, href\: string, verified\?\: bool\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/Nip05VerificationService.php
-
message: '#^Call to function is_object\(\) with object will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Service/Nip09DeletionApplier.php
-
message: '#^Call to an undefined method Symfony\\Component\\Security\\Core\\User\\UserInterface\:\:getRelays\(\)\.$#'
identifier: method.notFound
count: 2
path: src/Service/NostrClient.php
-
message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Service/NostrClient.php
-
message: '#^Call to function is_object\(\) with object will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 5
path: src/Service/NostrClient.php
-
message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 6
path: src/Service/NostrClient.php
-
-
message: '#^Method App\\Service\\NostrClient\:\:fetchKind5DeletionEventsForAuthors\(\) has invalid return type App\\Service\\stdClass\.$#'
message: '#^Method App\\Service\\NostrClient\:\:fetchKind5DeletionEventsForAuthors\(\) has invalid return type App\\Service\\stdClass\.$#'
identifier: class.notFound
identifier: class.notFound
count: 1
count: 1
path: src/Service/NostrClient.php
path: src/Service/NostrClient.php
-
message: '#^Offset ''dTags'' on array\{pubkey\: string, kind\: int\<1, max\>, dTags\: non\-empty\-list\<non\-empty\-string\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/NostrClient.php
-
message: '#^Offset ''kind'' on array\{pubkey\: string, kind\: int\<1, max\>, dTags\: non\-empty\-list\<non\-empty\-string\>\} in isset\(\) always exists and is not nullable\.$#'
identifier: isset.offset
count: 1
path: src/Service/NostrClient.php
-
message: '#^Offset ''pubkey'' on array\{pubkey\: string, kind\: int\<1, max\>, dTags\: non\-empty\-list\<non\-empty\-string\>\} in isset\(\) always exists and is not nullable\.$#'
identifier: isset.offset
count: 1
path: src/Service/NostrClient.php
-
message: '#^Offset ''pubkey'' on array\{pubkey\: string, kind\: int\<1, max\>, dTags\: non\-empty\-list\<non\-empty\-string\>\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/NostrClient.php
-
-
message: '#^PHPDoc tag @return with type swentel\\nostr\\Event\\Event\|null is not subtype of native type stdClass\|null\.$#'
message: '#^PHPDoc tag @return with type swentel\\nostr\\Event\\Event\|null is not subtype of native type stdClass\|null\.$#'
identifier: return.phpDocType
identifier: return.phpDocType
count: 1
count: 1
path: src/Service/NostrClient.php
path: src/Service/NostrClient.php
-
message: '#^Parameter \#1 \$array \(non\-empty\-list\<string\>\) of array_values is already a list, call has no effect\.$#'
identifier: arrayValues.list
count: 1
path: src/Service/NostrClient.php
-
message: '#^Result of \|\| is always false\.$#'
identifier: booleanOr.alwaysFalse
count: 1
path: src/Service/NostrClient.php
-
message: '#^Strict comparison using \=\=\= between non\-empty\-list\<non\-empty\-string\> and array\{\} will always evaluate to false\.$#'
identifier: identical.alwaysFalse
count: 1
path: src/Service/NostrClient.php
-
-
message: '#^PHPDoc tag @param for parameter \$event with type ArrayObject\<int, mixed\>\|list\<mixed\> is not subtype of native type object\.$#'
message: '#^PHPDoc tag @param for parameter \$event with type ArrayObject\<int, mixed\>\|list\<mixed\> is not subtype of native type object\.$#'
identifier: parameter.phpDocType
identifier: parameter.phpDocType
count: 1
count: 1
path: src/Service/NostrShareMenuBuilder.php
path: src/Service/NostrShareMenuBuilder.php
-
message: '#^Using nullsafe property access "\?\-\>value" on left side of \?\? is unnecessary\. Use \-\> instead\.$#'
identifier: nullsafe.neverNull
count: 1
path: src/Service/NostrShareMenuBuilder.php
-
message: '#^Offset ''label'' on array\{label\: string, href\: string\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: src/Service/ProfileIdentityLinksBuilder.php
-
message: '#^Offset 0 on non\-empty\-list\<string\> on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 2
path: src/Service/ProfileIdentityLinksBuilder.php
-
message: '#^Call to function is_object\(\) with object will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: src/Service/ProfilePaymentLinksBuilder.php
-
message: '#^Offset non\-falsy\-string on array\{\} in isset\(\) does not exist\.$#'
identifier: isset.offset
count: 1
path: src/Service/ProfilePaymentLinksBuilder.php
-
message: '#^Parameter \#1 \$array \(non\-empty\-list\<string\>\) of array_values is already a list, call has no effect\.$#'
identifier: arrayValues.list
count: 1
path: src/Service/ProfilePaymentLinksBuilder.php
-
message: '#^Strict comparison using \!\=\= between non\-empty\-list\<string\> and array\{\} will always evaluate to true\.$#'
identifier: notIdentical.alwaysTrue
count: 1
path: src/Service/ProfilePaymentLinksBuilder.php
-
-
message: '#^Call to protected method getEntityManager\(\) of class Doctrine\\ORM\\EntityRepository\<object\>\.$#'
message: '#^Call to protected method getEntityManager\(\) of class Doctrine\\ORM\\EntityRepository\<object\>\.$#'
identifier: method.protected
identifier: method.protected
count: 1
count: 1
path: src/Service/TopicIndexService.php
path: src/Service/TopicIndexService.php
-
message: '#^Property App\\Twig\\Components\\IndexTabs\:\:\$index is never read, only written\.$#'
identifier: property.onlyWritten
count: 1
path: src/Twig/Components/IndexTabs.php
-
message: '#^Call to function method_exists\(\) with App\\Entity\\Event and ''getTags'' will always evaluate to true\.$#'