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.