PHP Conventions and Smells
The following are rough guidelines that I find helpful for identifying code "smells" as well as conventions that I prefer to adhere to in my personal and professional PHP development work.
Code Style Using PSR1 and PSR12
The PHP Framework Interop Group describes a standard set of coding standards in their Basic Coding Standard and Extended Coding Style Guide. These seem to be the widest adopted coding standards in the PHP community.
The PHPCS toolset allows for automatic linting, and in some cases fixing of these issues. After installing it via composer you can run it via:
./vendor/bin/phpcs -p --standard=psr1 index.php
The key items from the [[psr]] style guides are outlined in a seperate note.
- Magic Numbers
- Magic numbers are the usage of arbitrarty numeric (or string) constants in code statements rather than defining these values as a constant
- Mutable Objects
- Favor the design of objects such that post-creation they are immutable. Methods on object accept parameters and return values based on the internal state of the object. Methods that would mutate the object state instead return a new instance of that object via the prototype pattern, e.g. a clone of the existing object with the new state. Ignore DDD's recommendation to avoid "anemic" models, it's fine to create objects that are merely structs to hold data with no methods except getters and all business logic in a seperate service class or collection of pure functions contained in the same module.
- Keep Function/Method Signatures Small
- This might not be possible with constructors, but avoid the creation of functions whose parameter list is exceptionally long or complex. Four or five parameters seems a maximum. One or two excellent. Zero is ideal.
- Seperate the Persistence and Model Layers
- A common mistake in PHP is mixing the persistence and model layers together. The Model layer should consist of classes that are containers for data and business logic for calculating values or mutating that data. A seperate layer should handle the issue of mapping or hydrating that data to and from the various persistence mediums (file system, database, rest-api). The repository pattern provides a means of accepting a concrete instance of some object and serializing it to the persistence medium. A read pattern (the two may be combined in a single class) handles the factory work of hydrating objects out of the persistence medium.
- Validate Requests Seperate from Models
- Create classes that validate the structure of an HTTP request for basic rules, e.g. that a date properties refer to valid dates, that string representations of integers validate to unsigned integer values, that a phone number or zip code are well formed, that required values are present. Use these classes to filter and normalize the structure of a request before it's values are passed to lower layers. Do not validate at this level that the request maps to valid values in the model, e.g. that an identity refers to an actual entity in the storage layer. Allow the application service layer to handle this validation.
- Avoid Application Logic in Controllers
- The controller is responsible for validating the request, marshalling the appropriate application services (repositories, application service classes, or models), and then formatting and returning the response (e.g. an html view object or json object). Do not add signifigant application logic into the controller, this should be in a well tested service class. That said, do not make superflous service classes -- if the entirety of the logic is to fetch a Model from a repository, set a property on it from the request, save it and return a 200 OK, then leave it in the controller.
- Never Mutate Objects Passed to a View
- A view can use various control structures (loops, conditionals) to determine the correct representation of it's data. However, it should not at this juncture be mutating or calculating the data itself. If you find a need in a view to calculate some sum from the values passed in or to mutate the property then this should be moved into the model. Do note, that formatting _is_ the job of the view.
- Keep Module Boundaries in Mind
- Architect around modular design. Always keep in mind when modular boundaries are being crossed, e.g. creating interdependencies between the classes or objects in two different modules. This is often where an interface instead of a concrete implementation should be used.