1. YoureCom
    1. Über mich
  2. Lösungen
    1. Tipprunde
    2. Aufgaben & Support
    3. Mitglieder-Netzwerk
    4. Online-Shop
    5. Alle Lösungen & Preise
  3. Für Admins
    1. Plugins
    2. ByteMon
    3. ByteMon: Community verwalten
    4. ByteMon: Portalidentität
    5. Individuelle Entwicklung
    6. WSC 6.3 Alpha
    7. GitHub
  4. Neuigkeiten
    1. Kurzmeldungen
  5. Support
  6. Mein Konto
  7. Lösungen testen
  • Anmelden
  • Registrieren
  • Warenkorb 0
  • Suche
Alles
  • Alles
  • Artikel
  • Seiten
  • Aufgaben
  • Produkte
  • Pulse-Beiträge
  • Erweiterte Suche
  1. YoureCom – Software für Vereine, Teams & Communities
  2. Repository-Liste
  3. de.yourecom.wcf.api.mpdf

43 Dateien geändert: tests (22), (Hauptordner) (15), .github (5), files (1)

  • Marcel Beckers
  • -4 Löschungen
  • +3.340 Hinzufügungen
  • 3.344 Gesamt
  • 43 Dateien
  • 14. Oktober 2026 um 08:08
  • tests/phpstan/OverrideOrDocblockRule.php
    <pre>@@ -0,0 +1,146 @@
    +<?php
    +
    +namespace yourecom\standard\phpstan;
    +
    +use PhpParser\Node;
    +use PHPStan\Analyser\Scope;
    +use PHPStan\Node\InClassMethodNode;
    +use PHPStan\Reflection\ClassReflection;
    +use PHPStan\Rules\IdentifierRuleError;
    +use PHPStan\Rules\Rule;
    +use PHPStan\Rules\RuleErrorBuilder;
    +
    +/**
    + * YoureCom Standard: every method has either `#[\Override]` (when it overrides or implements a method) or a docblock
    + * that explains it. With `#[\Override]` there is no explanation and no `@inheritDoc`, a docblock only keeps tags that
    + * PHPStan needs (e.g. `@param`, `@return`). Test methods of PHPUnit (`test*()` or `#[Test]`) need no docblock, their
    + * name describes the test. Magic methods that override nothing (`__construct()`, `__invoke()`, `__get()` …, where
    + * `#[\Override]` is not allowed) need no explanation either, PHP fixes their purpose; a docblock with `@inheritDoc` is
    + * fine.
    + * Managed by _dev/standard (apply.php), do not edit here.
    + *
    + * @author Marcel Beckers
    + * @license YoureCom License - Commercial (YCLC) <https://www.yourecom.de/shop/licence-commercial/>
    + * @implements Rule<InClassMethodNode>
    + */
    +final class OverrideOrDocblockRule implements Rule
    +{
    + #[\Override]
    + public function getNodeType(): string
    + {
    + return InClassMethodNode::class;
    + }
    +
    + #[\Override]
    + public function processNode(Node $node, Scope $scope): array
    + {
    + $method = $node->getOriginalNode();
    + $class = $node->getClassReflection();
    + $name = $method->name->toString();
    + $docComment = $method->getDocComment()?->getText();
    +
    + $hasOverride = false;
    + $isTest = false;
    + foreach ($method->attrGroups as $group) {
    + foreach ($group->attrs as $attribute) {
    + $attributeName = \ltrim($attribute->name->toString(), '\\');
    + if (\strcasecmp($attributeName, 'Override') === 0) {
    + $hasOverride = true;
    + }
    + if ($attributeName === 'PHPUnit\Framework\Attributes\Test') {
    + $isTest = true;
    + }
    + }
    + }
    +
    + // the name of a test method describes the test
    + if (($isTest || \str_starts_with($name, 'test')) && $class->isSubclassOf('PHPUnit\Framework\TestCase')) {
    + return [];
    + }
    +
    + $parent = $this->overriddenClass($class, $name);
    + if ($parent !== null && $name !== '__construct' && !$hasOverride) {
    + return [$this->error("Method {$class->getDisplayName()}::{$name}() overrides {$parent}::{$name}() but has no #[\\Override] attribute.", 'missing')];
    + }
    +
    + // the overridden method explains it, a docblock only keeps tags for PHPStan (e.g. @param, @return)
    + if ($hasOverride) {
    + if ($docComment !== null && \preg_match('~@inheritDoc~i', $docComment) === 1) {
    + return [$this->error("Method {$class->getDisplayName()}::{$name}() has #[\\Override], remove the @inheritDoc docblock.", 'inheritDoc')];
    + }
    + if ($docComment !== null && $this->hasDescription($docComment)) {
    + return [$this->error("Method {$class->getDisplayName()}::{$name}() has #[\\Override], remove the explanation from the docblock (keep only tags PHPStan needs).", 'description')];
    + }
    +
    + return [];
    + }
    +
    + // magic methods that override nothing cannot have #[\Override], PHP fixes their purpose (`@inheritDoc` is fine)
    + if (\str_starts_with($name, '__')) {
    + return [];
    + }
    + if ($docComment === null || !$this->hasDescription($docComment)) {
    + return [$this->error("Method {$class->getDisplayName()}::{$name}() needs a docblock that explains it (or #[\\Override] if it overrides a method).", 'docblock')];
    + }
    +
    + return [];
    + }
    +
    + /**
    + * Returns the name of the parent class, interface or trait whose method is overridden or implemented, null if none.
    + */
    + private function overriddenClass(ClassReflection $class, string $name): ?string
    + {
    + foreach ($class->getParents() as $parent) {
    + if ($parent->hasNativeMethod($name) && !$parent->getNativeMethod($name)->isPrivate()) {
    + return $parent->getDisplayName();
    + }
    + }
    + foreach ($class->getInterfaces() as $interface) {
    + if ($interface->hasNativeMethod($name)) {
    + return $interface->getDisplayName();
    + }
    + }
    + foreach ($class->getTraits(true) as $trait) {
    + if ($trait->hasNativeMethod($name) && $trait->getNativeMethod($name)->isAbstract()) {
    + return $trait->getDisplayName();
    + }
    + }
    +
    + return null;
    + }
    +
    + /**
    + * Tells whether a docblock has a line of text besides the tags (lines that continue a tag, like a multi-line
    + * array shape of `@return`, belong to the tag).
    + */
    + private function hasDescription(string $docComment): bool
    + {
    + $inTag = false;
    + foreach (\explode("\n", $docComment) as $line) {
    + $text = \trim(\preg_replace('~^\s*(?:/\*\*|\*/|\*)~', '', $line) ?? '');
    + $text = \trim(\preg_replace('~\*/$~', '', $text) ?? '');
    + if ($text === '') {
    + $inTag = false;
    + continue;
    + }
    + if (\str_starts_with($text, '@') || \str_starts_with($text, '{@')) {
    + $inTag = true;
    + continue;
    + }
    + if (!$inTag) {
    + return true;
    + }
    + }
    +
    + return false;
    + }
    +
    + /**
    + * Builds an error of this rule.
    + */
    + private function error(string $message, string $identifier): IdentifierRuleError
    + {
    + return RuleErrorBuilder::message($message)->identifier("yourecom.method.{$identifier}")->build();
    + }
    +}
  • tests/phpstan/rules.php
    <pre>@@ -0,0 +1,18 @@
    +<?php
    +
    +/**
    + * YoureCom Standard: registers the PHPStan rules of the standard. Managed by _dev/standard (apply.php), do not edit
    + * here. Included by phpstan.standard.neon: PHPStan builds its container before it loads bootstrapFiles, so the rule
    + * classes are loaded here, while the configuration is read.
    + */
    +
    +require_once __DIR__ . '/OverrideOrDocblockRule.php';
    +
    +return [
    + 'services' => [
    + [
    + 'class' => \yourecom\standard\phpstan\OverrideOrDocblockRule::class,
    + 'tags' => ['phpstan.rules.rule'],
    + ],
    + ],
    +];
  • tests/support/IntegrationTestCase.php
    <pre>@@ -0,0 +1,156 @@
    +<?php
    +
    +namespace yourecom\tests;
    +
    +use Laminas\Diactoros\ServerRequest;
    +use PHPUnit\Framework\TestCase;
    +use Psr\Http\Message\ResponseInterface;
    +use Psr\Http\Server\RequestHandlerInterface;
    +use wcf\data\user\User;
    +use wcf\system\user\storage\UserStorageHandler;
    +use wcf\system\WCF;
    +
    +/**
    + * YoureCom Standard: base class of the integration tests against the local dev system.
    + * Managed by _dev/standard (apply.php), do not edit here.
    + *
    + * Every test runs in a transaction that is rolled back and acts as the administrator (userID 1) by default.
    + */
    +abstract class IntegrationTestCase extends TestCase
    +{
    + protected const ADMIN_ID = 1;
    +
    + #[\Override]
    + protected function setUp(): void
    + {
    + WCF::getDB()->beginTransaction();
    + $this->actAs(self::ADMIN_ID);
    + }
    +
    + #[\Override]
    + protected function tearDown(): void
    + {
    + WCF::getDB()->rollBackTransaction();
    +
    + // the user storage writes its changes at the end of the request, pending changes of a test must not leak
    + $storage = UserStorageHandler::getInstance();
    + foreach (['log', 'cache'] as $property) {
    + (new \ReflectionProperty($storage, $property))->setValue($storage, []);
    + }
    + }
    +
    + /**
    + * Switches the active user (0 = guest). Permission values are strings in the CLI, a web request delivers integers
    + * for numeric options, so they are converted.
    + */
    + protected function actAs(int $userID): void
    + {
    + WCF::getSession()->changeUser(new User($userID > 0 ? $userID : null), true);
    + WCF::getSession()->getPermission('admin.general.canUseAcp');
    +
    + $property = new \ReflectionProperty(WCF::getSession(), 'groupData');
    + $groupData = $property->getValue(WCF::getSession());
    + if (\is_array($groupData)) {
    + foreach ($groupData as $name => $value) {
    + if (\is_string($value) && \preg_match('~^-?\d+$~', $value) === 1) {
    + $groupData[$name] = (int)$value;
    + }
    + }
    + $property->setValue(WCF::getSession(), $groupData);
    + }
    + }
    +
    + /**
    + * Returns the ID of the user with the given name, skips the test if the user does not exist.
    + */
    + protected function userID(string $username): int
    + {
    + $userID = self::scalar("SELECT userID FROM wcf1_user WHERE username = ?", [$username]);
    + if (!\is_numeric($userID)) {
    + self::markTestSkipped("The test needs the user '{$username}'.");
    + }
    +
    + return (int)$userID;
    + }
    +
    + /**
    + * Runs a PSR-15 request handler (action, form dialog) like a browser request.
    + *
    + * @param array<string, string> $query
    + * @param array<string, mixed>|null $body
    + */
    + protected static function handle(
    + RequestHandlerInterface $handler,
    + string $method = 'GET',
    + array $query = [],
    + ?array $body = null,
    + ): ResponseInterface {
    + $request = (new ServerRequest([], [], 'https://localhost/', $method))->withQueryParams($query);
    + if ($body !== null) {
    + $request = $request->withParsedBody($body);
    + }
    +
    + return $handler->handle($request);
    + }
    +
    + /**
    + * Decodes the JSON body of a response.
    + *
    + * @return array<string, mixed>
    + */
    + protected static function json(ResponseInterface $response): array
    + {
    + $data = \json_decode((string)$response->getBody(), true, flags: \JSON_THROW_ON_ERROR);
    + if (!\is_array($data)) {
    + return [];
    + }
    +
    + /** @var array<string, mixed> $data */
    + return $data;
    + }
    +
    + /**
    + * Returns the first column of the first row.
    + *
    + * @param list<int|float|string|null> $parameters
    + */
    + protected static function scalar(string $sql, array $parameters = []): mixed
    + {
    + $statement = WCF::getDB()->prepare($sql, 1);
    + $statement->execute($parameters);
    +
    + return $statement->fetchSingleColumn();
    + }
    +
    + /**
    + * Returns the first column of the first row as an integer (e.g. a COUNT(*) or an ID, 0 if there is no row).
    + *
    + * @param list<int|float|string|null> $parameters
    + */
    + protected static function int(string $sql, array $parameters = []): int
    + {
    + $value = self::scalar($sql, $parameters);
    +
    + return \is_numeric($value) ? (int)$value : 0;
    + }
    +
    + /**
    + * Returns a string field of a JSON response ('' if it is missing).
    + *
    + * @param array<string, mixed> $data
    + */
    + protected static function field(array $data, string $name): string
    + {
    + return \is_string($data[$name] ?? null) ? $data[$name] : '';
    + }
    +
    + /**
    + * Executes a statement.
    + *
    + * @param list<int|float|string|null> $parameters
    + */
    + protected static function execute(string $sql, array $parameters = []): void
    + {
    + WCF::getDB()->prepare($sql)->execute($parameters);
    + }
    +}
Es wurden keine Einträge gefunden.

Bereit, deine Community smarter zu machen?

Ob fertiges Produkt oder individuelle Softwarelösung – überzeuge dich selbst.

Jetzt kaufen Demo testen

Direkter Support vom Entwickler

Zuletzt erledigte Aufgaben

  1. Multihunter meldet jede Anmeldung als Doppelaccount Multihunter (Doppelaccount Jäger) Multihunter: Forum Benachrichtigungen

    villain Erledigt am 27. Oktober 2026 um 20:09
  2. Multihunter meldet jede Anmeldung als Doppelaccount Multihunter (Doppelaccount Jäger)

    villain Erledigt am 26. Oktober 2026 um 14:41
  3. Optionen vom "Dateien Feld-Typ" sprechen von Bildern Database: Medienfelder

    doerek Erledigt am 25. Oktober 2026 um 18:20
  4. Nach Installation ohne Popup Begriffserklärung

    mbbaureihen.de Erledigt am 24. Oktober 2026 um 23:33
  5. Fehlermeldung beim Hinzufügen einer neuen Clanseite Clan Verwaltung

    BMWTom Erledigt am 24. Oktober 2026 um 23:16
  6. neue Dateiendungen werden nicht übernommen. Database: Medienfelder

    doerek Erledigt am 24. Oktober 2026 um 19:46
Arbeitsplatz
YoureCom – Software für Vereine, Teams & Communities

YoureCom – Software für Vereine, Teams & Communities. Seit 2004.

Fertig eingerichtete Lösungen für Verein, Team und Community, dazu Erweiterungen für die WoltLab Suite. Aus Wuppertal, für Kunden weltweit.

Lösungen

  • Tipprunde
  • Aufgaben & Support
  • Mitglieder-Netzwerk
  • Online-Shop
  • Alle Lösungen & Preise

Für Administratoren

  • Plugins
  • Clan Verwaltung
  • Wettbewerbe
  • MultiHunter
  • ByteMon
  • Individuelle Entwicklung

YoureCom

  • Über mich
  • Shop
  • Kontakt
  • Neuigkeiten
  • GitHub

Cloud-Verträge

  • AGB Cloud
  • AVV Cloud
  • Rechtswidrige Inhalte melden
  • Datenwechsel

Rechtliches

  • Datenschutzerklärung
  • Impressum
  • AGB
  • Vertrag widerrufen
  • Abonnements kündigen
  • Widerrufsbelehrung
© 2026 YoureCom · Marcel Beckers