Skip to content

Coding Conventions

Die verbindliche Quelle ist phpcs.xml.dist. Diese Seite erklärt, was dort konfiguriert ist und welche Konventionen darüber hinaus im Code durchgängig angewendet werden.

Prüfen und korrigieren

bash
vendor/bin/phpcs     # meldet Verstöße
vendor/bin/phpcbf    # korrigiert, was automatisch korrigierbar ist

Was phpcs.xml.dist festlegt

Geprüfte Pfade: ad-form.php, uninstall.php, src/, templates/, tests/. Ausgeschlossen: vendor/, node_modules/, languages/. Geprüft wird nur die Endung php.

xml
<config name="testVersion" value="8.2-"/>
<config name="minimum_supported_wp_version" value="6.4"/>
<config name="text_domain" value="ad-form"/>

Aktive Rulesets:

RulesetBedeutung
WordPressVollständige WordPress Coding Standards (Core, Docs, Extra, Security)
PHPCompatibilityWPPrüft Syntax und Funktionen gegen PHP 8.2+
WordPress.WP.I18nErzwingt die Textdomain ad-form in allen i18n-Aufrufen

Bewusste Ausnahmen

WordPress.Files.FileName ist deaktiviert. Dateien heißen wie ihre Klasse (Plugin.php, FieldRegistry.php), nicht class-plugin.php – weil das Plugin PSR-4 autoloadet.

Mehrere Squiz.Commenting.*- und Generic.Commenting.*-Regeln sind ausgenommen, weil typisierte PHP-8.2-Signaturen ihre Parameter selbst dokumentieren. DocBlocks bleiben trotzdem Pflicht, wo sie echten Mehrwert liefern: Array-Shapes, Generics-artige Typen (list<FieldInterface>, array<string, mixed>) und die Signaturen von Hooks.

Weitere Ausnahmen: Generic.Formatting.MultipleStatementAlignment, WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound sowie WordPress.Security.EscapeOutput.ExceptionNotEscaped (Exception-Messages sind interne Entwicklertexte, keine Ausgabe).

Für tests/bootstrap.php sind zusätzlich WordPress.WP.I18n, WordPress.WP.AlternativeFunctions und Universal.Operators.DisallowShortTernary ausgenommen – der Bootstrap simuliert WordPress und darf sich nicht an dessen Regeln halten.

Formatierung (.editorconfig)

ini
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.php]
indent_style = tab
indent_size = 4

[*.{js,css,json,yml,yaml,md}]
indent_style = space
indent_size = 2

PHP wird also mit Tabs eingerückt (WordPress-Standard), alles andere mit zwei Leerzeichen.

PHP-Konventionen

Jede Datei beginnt gleich

php
<?php
/**
 * Kurze Beschreibung der Datei.
 *
 * @package Dingfelder\AdForm
 */

declare(strict_types=1);

namespace Dingfelder\AdForm\Fields;

declare(strict_types=1); steht ausnahmslos in jeder PHP-Datei, direkt nach dem Datei-DocBlock und vor dem namespace.

PSR-4 und Namespaces

Root-Namespace ist Dingfelder\AdForm\ mit dem Basisverzeichnis src/. Der Unterordner entspricht exakt dem Namespace-Segment:

KlasseDatei
Dingfelder\AdForm\Pluginsrc/Plugin.php
Dingfelder\AdForm\Core\Containersrc/Core/Container.php
Dingfelder\AdForm\Fields\Types\NumberFieldsrc/Fields/Types/NumberField.php

Tests liegen unter Dingfelder\AdForm\Tests\tests/.

Imports werden per use an den Dateikopf gezogen und alphabetisch sortiert; im Code stehen dann die kurzen Klassennamen.

final class als Standard

Konkrete Klassen sind final. Erweiterbarkeit entsteht über Interfaces und Hooks, nicht über Vererbung von Kernklassen. Nur die ausdrücklich als Basisklasse gedachten Typen sind abstract: AbstractField und AbstractAction.

php
final class FieldRegistry {
abstract class AbstractField implements FieldInterface {

Readonly Constructor Promotion

Abhängigkeiten werden im Konstruktor promoted und als readonly markiert. Damit sind Services unveränderlich und ihre Abhängigkeiten aus der Signatur ablesbar:

php
final class EmailAction extends AbstractAction {

	public function __construct(
		private readonly MailerInterface $mailer,
		private readonly MergeTagEngine $merge_tags
	) {}

Entities arbeiten mit with_*()-Methoden, die eine neue Instanz zurückgeben, statt Properties zu mutieren ($form->with_updated_at( Clock::now() )).

Yoda Conditions

WordPress erzwingt Yoda-Bedingungen: die Konstante steht links.

php
if ( '' === $error ) {
if ( ! is_array( $filtered ) ) {
if ( array() === $to ) {

Nicht if ( $error === '' ).

Typen überall

Parameter- und Rückgabetypen sind Pflicht, auch void. Wo PHP keinen präzisen Typ kennt, ergänzt ein DocBlock die Form:

php
/**
 * @param array<string, mixed> $settings Raw settings.
 * @return array<string, mixed>
 */
public function sanitize_settings( array $settings ): array {

Methodennamen in snake_case

Innerhalb des WordPress-Kontexts folgen Methoden- und Variablennamen snake_case (default_settings(), sanitize_submitted_value(), $form_id). Klassennamen sind StudlyCase, Konstanten UPPER_SNAKE_CASE.

Keine God Classes

Jede Klasse hat eine Verantwortung. Statt einer zentralen AdForm-Klasse gibt es getrennte Bausteine, die sich gegenseitig über den Container finden:

  • FieldRegistry kennt Feldtypen – nicht das Rendering-Detail.
  • FormRenderer erzeugt HTML – kennt keine Persistenz.
  • SubmitService validiert und speichert – versendet keine E-Mails.
  • ActionRunner führt Actions aus – kennt keine konkrete Action.

Neue Funktionalität kommt als neue Klasse plus Binding im passenden Service Provider, nicht als zusätzliche Methode in einer bestehenden Sammelklasse.

Sicherheit

  • Escapen bei der Ausgabe, nicht beim Speichern: esc_html(), esc_attr(), esc_url().
  • Sanitizing zentral über Dingfelder\AdForm\Security\Sanitizer (text(), textarea(), email(), key(), url(), int(), bool(), enum(), json_object()). Die Helfer rufen intern immer WordPress-APIs auf.
  • SQL immer über prepare() und den DatabaseInterface-Adapter, nie mit interpolierten Werten.
  • Capabilities statt Rollen prüfen (Dingfelder\AdForm\Security\Capabilities).
  • Nonces für alle schreibenden Requests (Dingfelder\AdForm\Security\Nonce).

Internationalisierung

Textdomain ist ad-form – als Literal, nie als Variable oder Konstante:

php
__( 'This field is required.', 'ad-form' )
esc_html__( 'AD Form requires PHP 8.2 or higher.', 'ad-form' )

Platzhalter-Strings bekommen einen Übersetzer-Kommentar:

php
$title = sprintf(
	/* translators: %s: original form title */
	__( '%s (Copy)', 'ad-form' ),
	$source->title()
);

Übersetzungsdateien liegen in languages/, geladen wird die Domain in Plugin::load_textdomain().

Hooks

Hook-Namen werden nicht als String im Code verstreut, sondern als Konstante in Dingfelder\AdForm\Core\Hooks gepflegt und dort referenziert. Jeder do_action()- und apply_filters()-Aufruf trägt einen DocBlock mit den Parametern – daraus entsteht die Hook-Referenz.

php
/**
 * Fires after a submission is stored. Honeypot hits never reach this hook.
 *
 * @param Submission $saved Saved submission.
 * @param Form       $form  Form.
 */
do_action( Hooks::AFTER_SUBMISSION, $saved, $form );

Digitale Lösungen. Persönlich. Zukunftssicher.