Skip to content

PHP Coding Guide

PHP coding best practices for the RightCapital backend team.

  1. All variables SHOULD be named carefully
  2. Variable names SHOULD be a noun, unless it’s a boolean or a callable
  3. Boolean variables CAN be an adjective, a third-person singular verb, or start with is, can, should
  4. Class names SHOULD be a noun unless it’s invokable (commands, jobs, event listeners, pipeline steps)
  5. Callable variables and invokable classes MAY be named with a verb
  6. Function and class method names SHOULD be a verb
  7. Trait names SHOULD be an adjective (Billable) or a third-person singular verb (InteractsWithRequest)

Use match expression instead of switch-case statements:

// Bad
switch($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'),
};

Methods and constants SHOULD be declared with minimal visibility.

Classes SHOULD be modified by final whenever possible.

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';
}
}

Classes and properties SHOULD be readonly whenever possible.

class Category {
protected readonly TweakSet $debt_tweak_set;
}
  • 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;
}

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);

Types MUST be declared whenever possible:

  1. Class properties and constants
  2. Function arguments and return values
  3. Closure arguments and return values
array_filter($colors, function (string $color, int $key): bool {
return $key === 2 || $color === 'blue';
}, ARRAY_FILTER_USE_BOTH);

Arrow function return types can be omitted only when bypassing to another function call:

// Bypass to another method
DB::transaction(fn () => $this->fix($check_result));
// Bypass to chain calling methods
ReadableScope::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'),
),
));

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);

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();

Use array bracket syntax:

$foo = $this->app['foo']; // recommended
$foo = $this->app->foo; // discouraged

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
}
}

Do NOT use $request->input() or $request->all(). Always use:

  • $request->query() - query parameters
  • $request->request() - POST body
  • $request->json() - JSON body

All console command handle() functions SHOULD return an integer following Unix exit code convention (0 = success, non-zero = failure).

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');