PHP Namespaces the right way

Namespaces in PHP are one of things that are left without a proper attention from the standard creators. While this gives a lot of space for creativity, this also results in a messy unpredictable packages structure, sometimes even when just one developer works on a package. Not to say when you work in a team.

The rules below are reflection of my experience of working on large projects in couple-of-pizzas teams.
All of them are based on the single principle: predictability.

Standard

Stick to PSR-4 Autoloader standard. A standard is good for predictability, and this one is a good one. If you are forced to use PSR-1 (or whatever) by the framework, that’s not a big deal. Just follow the rest of the rules, they are not dependant on PSR.

Public/internal

Put the classes that are a public API of your package as closer to the package root as it is possible.
Use deeper nesting level (sub-namespaces) for internal API – the components of the public API classes. Internal API classes should be treated as private.

Sub-namespaces

Sub-namespaces may be of three types:

  • components,
  • group of classes,
  • subpackage.

Some rules apply to each type.

Components

Assuming you are using composition and SRP, your public API classes will be built from smaller components represented by other classes
(a DDD Aggregate is a good example). Normally these components are only used by their container class and should be treated as private/protected.

Components are put into a sub-namespace named by the container class.

Example:

1
2
3
\Acme\ConfigReader           // Public API, uses Merger and Parser as components.
\Acme\ConfigReader\Merger // Internal API
\Acme\ConfigReader\Parser // Internal API

Here the \Acme\ConfigReader class is a public API, and no one except it should work directly with \Acme\ConfigReader\Merger or \Acme\ConfigReader\Parser.

Of course, a component can act as a container for its own sub-components, so this rule applies recursively.

Group of classes

Some design patterns, for example Strategy, which I use often, require several classes that do the same sort of a thing but somehow differently. These classes are often represent different implementation of some Type (or Interface if you like) and are used in a polymorphic manner.

Such classes may be put into a sub-namespace named by their Type in a plural form.

Example:

1
2
\Acme\ConfigReaders\Filesystem
\Acme\ConfigReaders\Database

The Type should not be duplicated in the class names.

1
2
// Incorrect
\Acme\ConfigReaders\FilesystemConfigReader

Quite often you would also need to put a Type interface somewhere and a Factory that creates a final implementation based on some input.
Here is a common structure:

1
2
3
4
\Acme\ConfigReaders\Filesystem  // Internal API (concrete implementation), implements \Acme\ConfigReaderInterface
\Acme\ConfigReaders\Database // Internal API (concrete implementation), implements \Acme\ConfigReaderInterface
\Acme\ConfigReaderFactory // Public API, creates isntance of \Acme\ConfigReaderInterface
\Acme\ConfigReaderInterface // Public API

Another use case for this technique is domain-level exceptions classes:

1
2
\Acme\Exceptions\SomethingWeirdHappened
\Acme\Exceptions\SomethingWentDrasticallyWrong

However in this case exceptions are a public API of the package.

Subpackages

Sometimes you might want to organize your package into a subpackages. This may be valid, for example, when your package is a plug-in that affects the behavior of different sub-systems, i.e.: Catalog, Cart and Checkout.

Respective classes are then put into a sub-namespace named by the subpackage. This approach is similar to the components, but there is no container class in this structure.

Example:

1
2
3
\Acme\Catalog\... (classes related to Catalog)
\Acme\Cart\... (classes related to Cart)
\Acme\Checkout\... (classes related to Checkout)

Grouping by subpackages only makes sense if you have many classes to put in each of them.
Otherwise you can just mention the respective sub-system in a class name:

1
2
3
\Acme\CatalogMessageRenderer
\Acme\CartTotalRow
\Acme\CheckoutTotalRow

Anyway I would suggest keeping your packages small. When you are going to introduce a subpackage consider creating a new package first.