PHP Coding Guide
PHP coding best practices for the RightCapital backend team.
Naming Semantics
Section titled “Naming Semantics”- All variables SHOULD be named carefully
- Variable names SHOULD be a noun, unless it’s a boolean or a callable
- Boolean variables CAN be an adjective, a third-person singular verb, or start with
is,can,should - Class names SHOULD be a noun unless it’s invokable (commands, jobs, event listeners, pipeline steps)
- Callable variables and invokable classes MAY be named with a verb
- Function and class method names SHOULD be a verb
- Trait names SHOULD be an adjective (
Billable) or a third-person singular verb (InteractsWithRequest)
Comparison
Section titled “Comparison”Use match expression
Section titled “Use match expression”Use match expression instead of switch-case statements:
// Badswitch($nullable_string_value) { case 'abc': $result = 'case 1'; break; case 'def': $result = 'case 2'; break; default: throw new UnexpectedValueException('invalid case');}
// Good$result = match($nullable_string_value) { 'abc' => 'case 1', 'def' => 'case 2', null => 'default case', default => throw new UnexpectedValueException('invalid case'),};Class Design
Section titled “Class Design”Visibility
Section titled “Visibility”Methods and constants SHOULD be declared with minimal visibility.
final keyword
Section titled “final keyword”Classes SHOULD be modified by final whenever possible.
static keyword
Section titled “static keyword”Class methods SHOULD be static whenever possible, unless the method needs to access non-static properties or methods.
class Foo{ protected int $id;
public function getId(): int { return $this->id; }
public static function getFoo(): string { return 'foo'; }}readonly keyword
Section titled “readonly keyword”Classes and properties SHOULD be readonly whenever possible.
class Category { protected readonly TweakSet $debt_tweak_set;}Constants
Section titled “Constants”- Use constants instead of protected static properties when values won’t change at runtime
- All constants MUST be declared with visibility
class B{ private const int GOOD = 0;}use keyword in namespace
Section titled “use keyword in namespace”Import namespaced classes and functions using use when there’s no name collision:
use \App\Models\Person;use \LogicException;
class ModelHelper{ public static function getPersonName(Person $person): string { if ($person->type === Person::TYPE_JOINT) { throw new LogicException('Cannot call ' . __METHOD__ . ' on joint person.'); } }}Exception: Keep namespace for Safe functions as a reminder:
$array = \Safe\json_decode($json, true);Type Declaration
Section titled “Type Declaration”Aggressive declaration
Section titled “Aggressive declaration”Types MUST be declared whenever possible:
- Class properties and constants
- Function arguments and return values
- Closure arguments and return values
array_filter($colors, function (string $color, int $key): bool { return $key === 2 || $color === 'blue';}, ARRAY_FILTER_USE_BOTH);Omittable arrow function return type
Section titled “Omittable arrow function return type”Arrow function return types can be omitted only when bypassing to another function call:
// Bypass to another methodDB::transaction(fn () => $this->fix($check_result));
// Bypass to chain calling methodsReadableScope::disable( fn () => Person::with(Person::RELATION_HOUSEHOLD)->findOrFail($person_id));
// Bypass to a class constructor$this->app->singleton('uploadcare', fn () => new UploadcareApi( Configuration::create( config('services.uploadcare.public_key'), config('services.uploadcare.secret_key'), ),));Array type declaration
Section titled “Array type declaration”Indexed arrays (lists): Use list<Type> in docblock:
/** @var list<int> $user_ids */$user_ids = get_user_ids();Associative arrays (maps): Declare key and value types:
/** @var array<int,string> $names_by_id */$names_by_id = get_names_by_id_given_types($types);Array structure description
Section titled “Array structure description”Use Array Shapes for complex arrays:
/** array<string,string> $array ['id' => string, 'value' => string] */$array = getArray();
/** array<string,array<string,mixed>> $array [['id' => int, 'value' => ?string]] */$array = getArray();Use ... for flexible/heterogeneous arrays:
/** array<string,mixed> ['id' => int, 'value' => ?string, ...] $array */$array = getArray();Laravel Conventions
Section titled “Laravel Conventions”Accessing application container
Section titled “Accessing application container”Use array bracket syntax:
$foo = $this->app['foo']; // recommended$foo = $this->app->foo; // discouragedAccessing Request
Section titled “Accessing Request”Use dependency injection. Do NOT use Request facade, request(), or app('request'):
class MyController extends Controller{ public function index(EmptyBodyRequest $request) { $foo = $request->query('foo'); // correct }}Differentiate query, request, and json
Section titled “Differentiate query, request, and json”Do NOT use $request->input() or $request->all(). Always use:
$request->query()- query parameters$request->request()- POST body$request->json()- JSON body
Console command return value
Section titled “Console command return value”All console command handle() functions SHOULD return an integer following Unix exit code convention (0 = success, non-zero = failure).
Foreign keys
Section titled “Foreign keys”MUST specify ON UPDATE CASCADE and ON DELETE RESTRICT:
$table->foreign('advisor_id', 'fk_category_advisor') ->references('user_id') ->on('advisors') ->onUpdate('CASCADE') ->onDelete('RESTRICT');