Software architecture & design - php-tuf/composer-stager GitHub Wiki

Directory structure

The overall directory structure conforms to the Standard PHP package skeleton specification, first of all, and then reflects the application architecture (below). cf. Screaming Architecture.

.
├── config # Application configuration, e.g., the services container.
├── docs # Documentation.
├── src # Production code, organized by application layer.
│   ├── API # The public API (aka, Domain) is covered by the BC promise and can be depended upon by external code.
│   └── Internal # Implementations (aka, Infrastructure) are not covered by the BC promise and cannot be depended upon by external code.
├── tests # Automated tests (PHPUnit).
├── tools # Custom static analysis and non-functional quality attribute testing.
│   ├── PHPBench
│   ├── PHPCS
│   └── PHPStan
├── var # Temporary files (created by testing tools) that do not get committed to the repository.
└── <root> # Standard config files, e.g., composer.json, phpcs.xml.dist.

Software architecture

The project aspires to a good domain-driven design. It begins with separate API (domain) and internal (infrastructure) layers (where the internal layer may depend on the API, but not the other way around). Classes are organized into services, value objects, aggregates, and other domain concepts. c.f. Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans.

The design details are enforced, as much as possible, by automated tools. See Automated testing & analysis.

Interface additions

Stable interfaces are essential to providing a reliable public API. In other words, once a PHP interface has been released, nothing can be added to it without breaking client implementations. Instead, if a new method is needed, it should be added to a new interface, even if it would otherwise cohere better with the first one. Take the following example:

  • An ExampleInterface in a stable release has a single method: ::one().
  • A client creates a MyExample implementation.
  • If a new method, ::two(), is added to ExampleInterface, MyExample will break as soon as it is released.
  • Instead, Example2Interface should be created for the new method. That way, MyExample will be unaffected.
  • Per Semver, Example2Interface should be released in the next minor version.
  • It may be deprecated later and merged into the original ExampleInterface in the next major version.

The obvious risk with this approach is proliferating lazy interfaces. It is therefore imperative to use it judiciously and strive for small, single-responsibility interfaces.

⚠️ **GitHub.com Fallback** ⚠️