Sinnvolle Namespaces in PHP

Eine übliche Konvention für Namespaces in PHP ist es, mit Vendor\Package zu beginnen, groß geschrieben (CamelCase StudlyCaps) mit “vendor” und “package” analog zum Composer Package Namen.

Es gibt allerdings eine schlechte Angewohnheit, die ich häufig sehe, vermutlich aus alten ZF1 und Pear Tagen, wo jedes Wort im Klassennamen ein neuer Sub-Namespace ist (und ein neues Unterverzeichnis), oder Kind-Klassen in einen Namespace mit dem Namen der Elternklasse platziert werden. All das führt zu tief geschachtelten Namespaces und Klassennamen, die keine Bedeutung ohne ihren Namespace haben.

Beispiele aus Zend Framework 1 (Pseudo Namespaces):

  • Zend_Db_Table_Row_Abstract ist eine abstrakte Basis-Klasse für Zend_Db_Table_Row, und repräsentiert eine Zeile in einer Datenbanktabelle. Es gibt auch noch Zend_Db_Table und Zend_Db.
  • Zend_Pdf_FileParser_Font_OpenType_TrueType ist ein Parser für True Type Schriftdateien. Die Klasse erbt von Zend_Pdf_FileParser_Font_OpenType, die wiederum von Zend_Pdf_FileParser_Font erbt, die von Zend_Pdf_FileParser erbt.

Und ein aktuelles Beispiel aus Magento 2:

  • Magento\Catalog\Model\Product\Option\Type\File\Validator – Ein Validator für Produkt-Optionen vom Typ “File”

Das resultiert in einer sehr verwirrenden Verzeichnisstruktur, tief geschachtelt und wo Klassen, die zusammengehören, nicht notwendigerweise nebeneinander liegen.

├── Option
│   ├── Converter.php
│   ├── ReadHandler.php
│   ├── Repository.php
│   ├── SaveHandler.php
│   ├── Type
│   │   ├── Date.php
│   │   ├── DefaultType.php
│   │   ├── Factory.php
│   │   ├── File
│   │   │   ├── ValidateFactory.php
│   │   │   ├── ValidatorFile.php
│   │   │   ├── ValidatorInfo.php
│   │   │   └── Validator.php
│   │   ├── File.php
│   │   ├── Select.php
│   │   └── Text.php
│   ├── Type.php
│   ├── UrlBuilder.php
│   ├── Validator
│   │   ├── DefaultValidator.php
│   │   ├── File.php
│   │   ├── Pool.php
│   │   ├── Select.php
│   │   └── Text.php
│   └── Value.php
├── Option.php

Das andere Extrem sind gar keine Sub-Namespaces, wobei alle Klassen-Dateien in einem Verzeichnis liegen (oder sogar alle in einer Datei)

Das ist völlig in Ordnung für kleine Packages mit etwa einem Dutzend Klassen, aber nicht mehr wenn es größer wird.

Da ich viele kleine Klassen gegenüber wenigen großen bevorzuge, muss ein Mittelweg gefunden werden. Andernfalls sieht die Code-Basis auf den ersten Blick komplex aus, wo sie es gar nicht ist, nur weil die Einzelteile wild durcheinander herumliegen, ohne sinnvolle Anordnung.

Was ist nun verkehrt mit den obigen Beispielen?

  • Unnötig tiefe Verschachtelung Nehmen wir die Zend Db Klassen: Datenbanken, Tabellen, Zeilen und Spalten (und mehr) machen ein Datenank-Schema aus und sind eng miteinander verkuppelt. Man wird keine Tabellen ohne Zeilen oder Zeilen ohne Tabellen haben. Wenn also all diese Klassen logisch zusammenhängen, warum sollte ich sie in verschiedenen Verzeichnissen suchen? Wenn ich den Code in der IDE oder auf Github durchsuche, möchte ich in der Lage sein schnell zwischen Klassen zu navigieren, die zusammengehören. Und wenn es unbekannter Code ist, möchte ich schnell erkennen, welche Klassen zusammengehören. Das ist einfacher, wenn sie an der selben Stelle liegen, etwa so:
    RelationalDbSchema
    ├── Database.php
    ├── Table.php
    ├── Row.php
    ├── Column.php
    

    Mit den PDF File Parser Klassen ist es ähnlich, wobei sie noch ein anderes Problem haben: Übermäßiger Gebrauch von Vererbung. Abgesehen davon könnte eine bessere Struktur so aussehen:

    FileParser
    ├── BaseFileParser.php
    ├── FontFileParser.php
    ├── OpenTypeFontFileParser.php
    ├── TrueTypeFontFileParser.php
    
  • Schwer zu findende Klassen Die meisten IDEs haben ein Feture zum Suchen von Klassen nach Namen, zum Beispiel für Code-Vervollständigung. Wenn man nun 50 Klassen “File” oder “Validation” in unterschiedlichen Namespaces hat, die für komplett verschiedene Dinge genutzt werden, dann macht man es schwer, die richtige zu finden. Wenn es andererseits eine einzige Klasse “FileOptionValidation” gibt, kann ich sie mit wenigen Tastenanschlägen finden und benutzen.

Hier sind also einige Regeln, die für mich bislang gut funktionieren:

Klassen organisiert halten

  • Namespace Tiefe so flach wie möglich halten. Das macht es einfacher, den Code zu navigieren.
  • Mit null beginnen, also Vendor\Package\Class, und führe Sub-Namespaces erst ein, wenn notwendig (siehe unten).
  • Wenn die Anzahl von Klassen in einem Namespace zu groß wird, um sich darin noch zurechtzufinden, denke darüber nach(!), ihn aufzuteilen 1 10 oder sogar 50 Klassen oder Interfaces in einem Namespace sind immer noch OK, wenn sie wirklich zusammengehören.
  • Betrachte jeden Sub-Namespace als Separate Komponente und halte die Koppelung zwischen den Komponenten klein. Das wird helfen, die Namespaces auf sinnvolle Art zu organisieren.
  • Der Klassename sollte immer aussagekräftig genug sein, auch ohne den Namespace zu funktionieren (also ProductRepository anstatt Product\Repository). Das wird dabei helfen, schnell die richtigen Klassen zu finden. Außerdem kann man die IDE sie automatisch importieren lassen, ohne dass es notwendig wird, ständig Aliase zu definieren.
  • Vermeide Namespaces, die beides enthalten, Klassen und Sub-Namespaces. Das ist die am wenigsten wichtige Regel, aber ich denke, sie kann eine nützliche Richtlinie sein, um die Distanz zwischen Klassen die zusammen gehören, gering zu halten. Eine Ausnahme kann eine Klasse sein, die als einziger Einstiegspunkt für ein Package fungiert, das Sub-Namespaces hat. Diese Klasse wird als einzige auf der obersten Ebene liegen, neben den Sub-Namespaces, so dass es offensichtlich ist, dass das die Stelle ist, bei der man anfängt, das Package zu nutzen.

    Reddit-Nutzer LtAramaki brachte ein interessantes Argument ein:

    I can see you seem to equate “namespace” and “package”, but they really aren’t the same thing

    Ich habe darüber nachgedacht und konnte keine Argumente gegen eine Klasse à la Vendor\Package als Einstiegspunkt für ein Package finden. Ich werde es in Zukunft in Betracht ziehen.

Ich halte mich weitestgehend an die “eine Klasse pro Datei” Konvention und PSR-4, also Namespaces abgebildet auf Verzeichnisse. Die Gründe sind praktischer Natur: Autoloader-Kompatibilität und das “Prinzip der geringsten Überraschung”.

“Private Klassen” in der Datei einer anderen Klasse sind eine seltene Ausnahme, und nur wenn sie nirgendwo außerhalb dieser “Oberklasse” genutzt werden.

Und wie organisiert ihr so eure Namespaces?

5 Replies to “Sinnvolle Namespaces in PHP”

  1. Nice post and I totally agree.
    Furthermore I like classnames having a postfix indicating their type.
    Like in the piece of code you are showing above, you have a Validator directory presumably containing different validators (File, Text, Select). Supposedly those are all validators, I would prefer there names to be FileValidator, TextValidator, SelectValidator.

  2. Great post!

    A quick clarification that camel case actually starts with a lowercase (camelCase). Class names as defined by PSR-1 are to be StudlyCaps – https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md#3-namespace-and-class-names.

    I loved all of the examples of overly complicated or nested namespaces. I don’t like apps that I have to go through multiple folders to get to a single class. Luckily GitHub direct links if directories are empty save for a child file.

    Your comment on ProductRepository vs Product\Repository is good, but I think it depends on the use case and size of the app. On small, simple apps, I will add the type suffix (ie: Repository). On larger apps, I opt for organization without suffixes. With medium sized apps, I’ll simply group all of my repositories so I’d end up with Repositories\Product. Or in larger apps I opt for a more DDD approach and have all things “Product” grouped and end up with Product\Repository.

    So, yeah, there is more than one way to skin a cat, and I think it just depends on the needs and size of the project. But you’re spot on with the “wrong” ways. I prefer to follow the standards of autoloading instead of underscores, and I think as a community we’re better for having those.

    Thanks for the post!

  3. I’m ceating my own framework. (Yes, im one of `those` guys…)

    – A namespace is mapped to a file. Thus more than one class in one file.
    – Namespace is ‘prefixed’ with it’s type, like: api/vendor/package/MyInterfaceDefinition

    Thus i can have files that look like this:
    vendor/package/myDefinition.api.php
    vendor/package/myImplementiation.src.php <== 'default' if no prefix is used.
    vendor/package/myExceptions.exc.php
    vendor/package/myXxxxx.model.php
    vendor/package/myXxxxx.viewModel.php
    vendor/package/myXxxxx.controller.php

    I also can have a special prefix: build/…/…
    That just means: "hey, autoloader. If you can't find it, please let me know. I wil build/create it for you"

    Names like: "default, class, object, string, exc, api, model"… can't be vendors, because of prefixes and invalid namespace names.

    Advantage: I don't need a slow and complex autoloader. It's just a simple mapping that is a 'constant'. I like "constants"….My multi-core computer likes them also 🙂

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *