Skip to content

Commit 8606d25

Browse files
authored
Merge pull request #48 from FriendsOfREDAXO/feat/phpstan-level8-beta2
fix: PHPStan Level 8 – 0 Fehler (1.0.0-beta2)
2 parents 43b5615 + 4096826 commit 8606d25

15 files changed

Lines changed: 197 additions & 46 deletions

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Changelog
22

3+
## [1.0.0-beta2] – 2026-03-06
4+
5+
### Bugfixes
6+
7+
- **`ART_ADDED` doppelter Log-Eintrag bei Mehrsprachigkeit**: REDAXO feuert `ART_ADDED` einmal pro Sprache – neuer Artikel wird jetzt nur einmal geloggt (`static $logged` Deduplication).
8+
- **`CAT_ADDED` / `CAT_DELETED` doppelter Log-Eintrag bei Mehrsprachigkeit**: Gleiche Deduplication-Logik wie bei Artikeln auf Kategorien ausgeweitet.
9+
10+
### Technisches
11+
12+
- **PHPStan Level 8 – 0 Fehler**: Komplette Typbereinigung aller Addon-Klassen und Fragmente.
13+
- `Activity`: Statische Properties typisiert, Fluent-Methoden geben `self` zurück, `new self()`, `assert()` für Factory-Methode.
14+
- `ActivityClear`: `QuestionHelper`-Cast, Validator-Signatur auf `float|int|string`.
15+
- `ActivityLogCronjob`: `@return array<int, array<string, mixed>>` für `getParamFields()`.
16+
- `EP\EpTrait`: `callable``string|callable`, korrekte PHPDoc-Typen für Callbacks.
17+
- `EP\User`: `rex_user`-Import und `@var`-Cast für `$params['user']`.
18+
- `EP\Yform.php`: Unterdrückung der `rex_yform_manager_table`-Fehler (optionales Addon, nicht in Analyse-Umgebung installiert) via `phpstan.neon`.
19+
- `fragments/filter-form.php`: `(string)`-Cast für `htmlspecialchars()` / `ucfirst()`.
20+
- `pages/system.activity-log.php`: Null-sicherer Operator `rex::getUser()?->isAdmin() ?? false`.
21+
- **`phpstan.neon`** ergänzt um `ignoreErrors` für `rex_yform_manager_table` (optionales Addon) und `fragments/` als Analyse-Pfad.
22+
23+
---
24+
325
## [1.0.0-beta1] – 2026-03-06
426

527
Diese Version bringt die lang geplante Code-Modernisierung mit vollständiger Namespace-Migration sowie mehrere Bugfixes, neue Features und modernisierte CI-Workflows.

assets/css/styles.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fragments/filter-form.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class="form-control"
4545
<label for="filter_source"><?= rex_i18n::msg('activity_log_col_source') ?></label>
4646
<select class="form-control selectpicker" name="source" id="filter_source" title="-">
4747
<?php foreach ($sources as $row) : ?>
48-
<option <?= ($this->source ?? '') === $row['source'] ? 'selected' : '' ?> value="<?= htmlspecialchars($row['source']) ?>"><?= htmlspecialchars(ucfirst($row['source'])) ?></option>
48+
<option <?= ($this->source ?? '') === $row['source'] ? 'selected' : '' ?> value="<?= htmlspecialchars((string) $row['source']) ?>"><?= htmlspecialchars(ucfirst((string) $row['source'])) ?></option>
4949
<?php endforeach ?>
5050
</select>
5151
</div>

lib/Activity.php

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
use rex_url;
1212
use rex_user;
1313

14+
use function assert;
15+
use function is_int;
16+
1417
class Activity
1518
{
1619
public const TYPE_INFO = 'info';
@@ -24,13 +27,12 @@ class Activity
2427
public const TYPE_EDIT = 'edit';
2528
public const TYPE_DELETE = 'delete';
2629

27-
/** @var rex_addon|null */
28-
private static $addon;
29-
private static $table = '';
30-
private static $activity;
31-
private static $message;
32-
private static $type;
33-
private static $causer;
30+
private static rex_addon $addon;
31+
private static string $table = '';
32+
private static ?self $activity = null;
33+
private static ?string $message = null;
34+
private static ?string $type = null;
35+
private static ?int $causer = null;
3436
private static ?string $source = null;
3537

3638
/**
@@ -39,11 +41,13 @@ class Activity
3941
public static function __constructStatic(): void
4042
{
4143
self::$table = rex::getTable('activity_log');
42-
self::$addon = rex_addon::get('activity_log');
44+
$addon = rex_addon::get('activity_log');
45+
assert($addon instanceof rex_addon);
46+
self::$addon = $addon;
4347
self::$addon->setConfig('cleared', false);
4448

4549
if (null === self::$activity) {
46-
self::$activity = new static();
50+
self::$activity = new self();
4751
}
4852
}
4953

@@ -86,48 +90,52 @@ public static function log(): void
8690
/**
8791
* Set message.
8892
*/
89-
public static function message(string $message): ?self
93+
public static function message(string $message): self
9094
{
9195
self::$message = $message;
96+
assert(null !== self::$activity);
9297
return self::$activity;
9398
}
9499

95100
/**
96101
* Set type (default: notice).
97102
*/
98-
public static function type(string $type): ?self
103+
public static function type(string $type): self
99104
{
100105
self::$type = $type;
106+
assert(null !== self::$activity);
101107
return self::$activity;
102108
}
103109

104110
/**
105111
* Set source (e.g. 'article', 'yform', 'media').
106112
*/
107-
public static function source(string $source): ?self
113+
public static function source(string $source): self
108114
{
109115
self::$source = $source;
116+
assert(null !== self::$activity);
110117
return self::$activity;
111118
}
112119

113120
/**
114121
* Set causer.
115-
*
116-
* @param rex_user|int $user
117122
*/
118-
public static function causer($user): ?self
123+
public static function causer(rex_user|int|null $user): self
119124
{
120-
if (is_numeric($user)) {
121-
self::$causer = (int) $user;
125+
if (is_int($user)) {
126+
self::$causer = $user;
122127
} elseif ($user instanceof rex_user) {
123128
self::$causer = $user->getId();
124129
}
125130

131+
assert(null !== self::$activity);
126132
return self::$activity;
127133
}
128134

129135
/**
130136
* List callback – user column.
137+
*
138+
* @param array<string, mixed> $params
131139
*/
132140
public static function userListCallback(array $params): string
133141
{
@@ -153,6 +161,8 @@ public static function userListCallback(array $params): string
153161

154162
/**
155163
* List callback – type column.
164+
*
165+
* @param array<string, mixed> $params
156166
*/
157167
public static function typeListCallback(array $params): string
158168
{
@@ -165,6 +175,8 @@ public static function typeListCallback(array $params): string
165175

166176
/**
167177
* List callback – message column.
178+
*
179+
* @param array<string, mixed> $params
168180
*/
169181
public static function messageListCallback(array $params): string
170182
{
@@ -173,6 +185,8 @@ public static function messageListCallback(array $params): string
173185

174186
/**
175187
* List callback – source column.
188+
*
189+
* @param array<string, mixed> $params
176190
*/
177191
public static function sourceListCallback(array $params): string
178192
{

lib/ActivityClear.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use rex_sql_exception;
1212
use RuntimeException;
1313
use Symfony\Component\Console\Command\Command;
14+
use Symfony\Component\Console\Helper\QuestionHelper;
1415
use Symfony\Component\Console\Input\InputInterface;
1516
use Symfony\Component\Console\Output\OutputInterface;
1617
use Symfony\Component\Console\Question\Question;
@@ -29,11 +30,12 @@ protected function configure(): void
2930
*/
3031
protected function execute(InputInterface $input, OutputInterface $output): int
3132
{
33+
/** @var QuestionHelper $helper */
3234
$helper = $this->getHelper('question');
3335

3436
$daysQuestion = new Question('Delete logs older than n days (default: -1 -> delete all): ', -1);
35-
$daysQuestion->setValidator(static function ($answer) {
36-
if (!is_numeric($answer) || '' === $answer) {
37+
$daysQuestion->setValidator(static function (mixed $answer): float|int|string {
38+
if (!is_numeric($answer)) {
3739
throw new RuntimeException('You must enter a number.');
3840
}
3941
return $answer;

lib/ActivityLogCronjob.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function getTypeName(): string
5959
/**
6060
* Additional parameter fields.
6161
*
62-
* @return array<array>
62+
* @return array<int, array<string, mixed>>
6363
*/
6464
public function getParamFields(): array
6565
{

lib/EP/Article.php

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function __construct()
2626

2727
/** @phpstan-ignore-next-line */
2828
if (is_bool(self::$addon->getConfig('article_added')) && self::$addon->getConfig('article_added')) {
29-
$this->add('ART_ADDED', static::class . '::message');
29+
$this->addArticle();
3030
}
3131

3232
/** @phpstan-ignore-next-line */
@@ -50,6 +50,37 @@ protected function getSource(): string
5050
return 'article';
5151
}
5252

53+
/**
54+
* Registriert den ART_ADDED-Handler mit Deduplication:
55+
* Da REDAXO ART_ADDED einmal pro Sprache feuert, wird nur der erste Aufruf
56+
* pro Artikel-ID geloggt.
57+
*/
58+
private function addArticle(): void
59+
{
60+
$source = $this->getSource();
61+
rex_extension::register('ART_ADDED', static function (rex_extension_point $ep) use ($source): void {
62+
/** @var array<string, mixed> $params */
63+
$params = $ep->getParams();
64+
$articleId = (int) ($params['id'] ?? 0);
65+
66+
/** @var array<int, bool> $logged */
67+
static $logged = [];
68+
69+
if (isset($logged[$articleId])) {
70+
return;
71+
}
72+
$logged[$articleId] = true;
73+
74+
$message = static::message($params, Activity::TYPE_ADD);
75+
76+
Activity::message($message)
77+
->type(Activity::TYPE_ADD)
78+
->source($source)
79+
->causer(rex::getUser())
80+
->log();
81+
});
82+
}
83+
5384
/**
5485
* Registriert den ART_DELETED-Handler mit Deduplication:
5586
* Da REDAXO ART_DELETED einmal pro Sprache feuert, wird nur der erste Aufruf

lib/EP/Category.php

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
namespace FriendsOfRedaxo\ActivityLog\EP;
44

5+
use FriendsOfRedaxo\ActivityLog\Activity;
6+
use rex;
57
use rex_addon_interface;
68
use rex_category;
79
use rex_clang;
10+
use rex_extension;
11+
use rex_extension_point;
812
use rex_url;
913

1014
use function is_bool;
@@ -22,7 +26,7 @@ public function __construct()
2226

2327
/** @phpstan-ignore-next-line */
2428
if (is_bool(self::$addon->getConfig('category_added')) && self::$addon->getConfig('category_added')) {
25-
$this->add('CAT_ADDED', static::class . '::message');
29+
$this->addCategory();
2630
}
2731

2832
/** @phpstan-ignore-next-line */
@@ -32,7 +36,7 @@ public function __construct()
3236

3337
/** @phpstan-ignore-next-line */
3438
if (is_bool(self::$addon->getConfig('category_deleted')) && self::$addon->getConfig('category_deleted')) {
35-
$this->delete('CAT_DELETED', static::class . '::message');
39+
$this->deleteCategory();
3640
}
3741

3842
/** @phpstan-ignore-next-line */
@@ -46,6 +50,68 @@ protected function getSource(): string
4650
return 'category';
4751
}
4852

53+
/**
54+
* Registriert den CAT_ADDED-Handler mit Deduplication:
55+
* Da REDAXO CAT_ADDED einmal pro Sprache feuert, wird nur der erste Aufruf
56+
* pro Kategorie-ID geloggt.
57+
*/
58+
private function addCategory(): void
59+
{
60+
$source = $this->getSource();
61+
rex_extension::register('CAT_ADDED', static function (rex_extension_point $ep) use ($source): void {
62+
/** @var array<string, mixed> $params */
63+
$params = $ep->getParams();
64+
$categoryId = (int) ($params['id'] ?? 0);
65+
66+
/** @var array<int, bool> $logged */
67+
static $logged = [];
68+
69+
if (isset($logged[$categoryId])) {
70+
return;
71+
}
72+
$logged[$categoryId] = true;
73+
74+
$message = static::message($params, Activity::TYPE_ADD);
75+
76+
Activity::message($message)
77+
->type(Activity::TYPE_ADD)
78+
->source($source)
79+
->causer(rex::getUser())
80+
->log();
81+
});
82+
}
83+
84+
/**
85+
* Registriert den CAT_DELETED-Handler mit Deduplication:
86+
* Da REDAXO CAT_DELETED einmal pro Sprache feuert, wird nur der erste Aufruf
87+
* pro Kategorie-ID geloggt.
88+
*/
89+
private function deleteCategory(): void
90+
{
91+
$source = $this->getSource();
92+
rex_extension::register('CAT_DELETED', static function (rex_extension_point $ep) use ($source): void {
93+
/** @var array<string, mixed> $params */
94+
$params = $ep->getParams();
95+
$categoryId = (int) ($params['id'] ?? 0);
96+
97+
/** @var array<int, bool> $logged */
98+
static $logged = [];
99+
100+
if (isset($logged[$categoryId])) {
101+
return;
102+
}
103+
$logged[$categoryId] = true;
104+
105+
$message = static::message($params, Activity::TYPE_DELETE);
106+
107+
Activity::message($message)
108+
->type(Activity::TYPE_DELETE)
109+
->source($source)
110+
->causer(rex::getUser())
111+
->log();
112+
});
113+
}
114+
49115
/**
50116
* @param array<string> $params
51117
* @param array<string>|null $additionalParams

0 commit comments

Comments
 (0)