PHP implementation of Elegant Objects.
This is a theory. All code examples are abstract.
- Elegant PHP objects doctrine
Try to stay under 250 lines and lower.
Try to create 5 or less number of public methods.
Use small interfaces, one object can implement a number of them.
Objects must be very cohesive (their methods and properties should be close to each other).
Try to encapsulate 4 or less objects in one object.
Make sure that all public methods in your class is an implementation of an interface.
Examples:
- Bad
interface Foo
{
public function bar(): void;
}
class DefaultFoo implements Foo
{
public function bar(): void {}
public function baz(): void {}
}
- Better
interface Bar
{
public function bar(): void;
}
interface Baz
{
public function baz(): void;
}
final class Foo implements Bar, Baz
{
public function bar(): void {}
public function baz(): void {}
}
Make classes final or abstract. Make abstract class methods final (don't override abstract class methods).
Extend types(interfaces and abstract classes) only.
Examples:
- Bad
class Foo
{
public function baz()
{
// foo implementation
}
public function quux()
{
// foo implementation
}
}
class Bar extends Foo
{
public function quux()
{
// bar implementation
}
}
- Better
interface Baz
{
public function baz(): void;
}
interface Quux extends Baz
{
public function quux(): void;
}
interface Foo extends Baz, Quux
{
}
final class DefaultFoo implements Foo
{
public function baz()
{
// foo implementation
}
public function quux()
{
// foo implementation
}
}
final class DefaultBar implements Bar, Baz
{
public function __construct(
private readonly Foo $foo
) { }
public function baz()
{
$this->foo->baz();
}
public function quux()
{
// bar implementation
}
}
Each class should have a unit test.
Unit test names should shows what objects really do.
Each interface should have a fake object.
Use fake objects instead of mock and stub objects.
Examples:
- Bad
class SaveCommentTest
{
public function testWithValidUser()
{
$userRepository = $this->createMock(UserRepository::class);
$userRepository
->method('getById')
->willReturn(new User(/* some data */));
arrest_that($this->saveCommentService->save($userRepository, 'message'));
}
}
- Better
interface PgSqlUser
{
public function userById(IntegerType $id): User;
}
class PgSqlFakeUser implements PgSqlUser
{
public function userById(IntegerType $id): User
{
return $this->user;
}
}
final class PutCommentActionTest implements TestCase
{
public function susscessful(): void
{
assert_that(
(new PutCommentAction(new PgSqlFakeUser(), 'message'))->succsessful()
);
// the same with temporal coupling:
//
// $user = new PgSqlFakeUser();
// $action = new PutCommentAction($user, 'message');
//
// arrest_that($action->succsessful());
}
public function failed(): void
{
assert_not(
(new PutCommentAction(new PgSqlFakeUser()))->succsessful()
);
}
}
Try to write code where each line is independent, otherwise replacing the lines will cause a syntax error.
Examples:
- Bad
final class DefaultFoo implements Foo
{
public function process(Dependency $dependency): void
{
// if we swap the return statements below, the behavior will be different without an error
// it is a hidden coupling, the first "return" depends on the "if" condition
if ($dependency->someExpected()) {
return $this->bar();
}
return $this->baz();
}
}
- Better
final class BarFoo implements Foo
{
public function process()
{
// bar
}
}
final class BazFoo implements Foo
{
public function process()
{
// baz
}
}
$foo = new ObjectiveIf(
condition: $dependency,
then: new BarFoo,
else: new BazFoo,
);
Try to use vertical instead of horizontal decomposition of responsibility.
Examples:
- Bad
final class FileLog implements Log
{
public function __construct(
private readonly string $path
) {}
public function put(Message $message): void
{
file_put_contents($this->path, $message->value());
}
}
final class TimedMessage implements Message, StringType
{
public function __construct(
private readonly string $message
) {}
public function value(): string
{
return sprintf("%s: %s", date('Y-m-d'), $this->message);
}
}
// the script below knows both Log and Message interfaces, it's a horizontal decomposition of responsibility
$log = new FileLog('/path/to/log');
$message = new TimedMessage('message');
$log->put($message);
- Better
final class TimedLog implements Log
{
public function __construct(
private readonly Log $log
) {}
public function put(string $message): void
{
$this->log->put(sprintf("%s: %s", date('Y-m-d'), $message));
}
}
// now we only use the Log interface, it's a vertical decomposition of responsibility
$log = new TimedLog(new FileLog('/path/to/log'));
$log->put('some message');
Class names must be named by what them objects are, not what they do.
Use nouns and avoid postfix "-er".
Examples:
- Bad
class DataProvider
{
public function __construct(
private readonly QueryBuilder $queryBuilder
) { }
public function getData(string $table, array $select, array $where): array
{
$query = $this
->queryBuilder
->select($select)
->from($table)
->where($where);
return $this->fetchAll();
}
}
- Better
final class FooSQLReport implements FooReport
{
public function __construct(
private readonly DbConnection $connection,
private readonly TableName $tableName
) { }
public function asArrayBetweenDates(DateTime $from, DateTime $to): ArrayType
{
return $this->connection->sqlResultAsArray(
sql: 'SELECT * FROM ?s ...',
tableName: this->tableName->value(),
);
}
}
Use nouns when a method will create something and return it.
Use verbs when a method manipulates.
Use adjectives when a method returns a boolean value (or make those values objects and use nouns).
Never mix builders and manipulators together.
Examples:
- Bad
class File
{
public function save(string $path): bool
{
$succsessful = file_put_contents($path, $this->file);
return (bool)$successful;
}
}
- Better
final class FileSavingResult implements Result
{
public function __construct(
private readonly bool $successful = false
) { }
public function from(bool $result): static
{
return new static($result);
}
public function successful()
{
return $this->successful;
}
}
final class MyFile implements File
{
public function __construct(
private readonly ResourceFile $file,
private readonly Result $result
) {}
public function savedTo(StringType $path): static
{
return new static(
file: $this->file,
result: $this->result->from(file_put_contents($path, $this->file))
);
}
public function successful(): bool
{
return $this->result->successful();
}
}
Use single and plural nouns for variable names, or refactor the code.
Use a noun with an adjective if the variable losses its meanings without the adjective (for example: 'timeZone', 'microService').
Examples:
- Bad
class DaysHelper
{
public function getWorkingDays()
{
$allDays = $this->repository->getDays();
$workingDays = [];
foreach ($allDays as $day) {
if ($day->isWorking()) {
$workingDays[] = $day;
}
}
return $workingDays;
}
}
- Better
final class WorkingDays implements Days
{
public function __construct(
private readonly AllDays $period,
private Collection $collection
) { }
public function asArray(): array
{
// it's better to create an objective mapping: new Map(...)
foreach ($this->period as $day) {
if (!$day->working()) {
continue;
}
$this->collection->add($day);
}
return $this->collection->asArray();
}
}
Objects must be immutable, but they can represent mutable data, such as a file on disk.
Examples:
- Bad
class Foo
{
private string $bar;
public function setBar(string $bar)
{
$this->bar = $bar;
}
}
- Better
final class DefaultFoo implements Foo
{
public function __construct(
private readonly StringType $bar
) {}
}
Object creation should be flexible. Try to keep number of object creation ways greater then number of object methods.
Emulate a "primary constructor" after others.
Examples:
- Bad
class Foo
{
public function __construct()
{
// empty
}
public function createFromInt(int $input)
{
// Foo creation
}
public function createFromString(string $input)
{
// Foo creation
}
}
- Better
final class DefaultFoo implements Foo
{
private SomeType $data;
/**
* @param array<string,StringType> type
*
* @throws TypeException
*/
public function __construct(array $type) // you can design it better
{
if (array_key_exists('string', $type)) {
$this->data = new SomeTypeFromString($type['string']);
} else if (array_key_exists('int', $type)) {
$this->data = new SomeTypeFromInt($type['int']);
}
// primary constructor below
throw new TypeException('undefined type');
}
}
Constructors must encapsulate data and must not manipulate it.
Examples:
- Bad
class Foo
{
private int $value;
public function __construct(string $inputValue)
{
$this->value = (int)$inputValue; // type conversion behavior
}
}
- Better
interface IntegerType
{
public function value();
}
final class StringAsInteger implements IntegerType
{
public function __construct(
private readonly StringType $value
) {}
public function value()
{
return (int)$this->value;
}
}
final class DefaultFoo implements Foo
{
private readonly IntegerType $value;
public function __construct(StringType $value)
{
$this->value = new StringAsInteger($value);
}
}
Try to encapsulate 4 or less objects in one object.
Examples:
- Bad
class User
{
private int $id;
private string $firstName;
private string $lastName;
private string $country;
private string $city;
private string $street;
private bool $isAdmin;
private bool $canViewSomeReport;
}
- Better
final class AuthorizedUser implements User
{
private readonly PersonalInformation $personalInformation;
private readonly Address $address;
private readonly Role $role;
}
Objects must not have an empty body.
Examples:
- Bad
class Year
{
public function current()
{
$dateTime = new DateTime();
return $dateTime->format('Y');
}
}
- Better
final class Year implements StringType
{
public function __construct(
private DateTime $date
) {}
public function value(): string
{
return $this->date->format('Y');
}
}
The state of objects must be their identity.
Examples:
- Bad
class Foo
{
public function __construct(
private readonly SomeString $string
) { }
}
$a = new Foo('123');
$b = new Foo('123');
if ($a === $b) { // will be false
// do something
}
- Better
final class DefaultFoo implements Foo
{
public function __construct(
private readonly SomeString $string
) { }
}
$a = new Foo('123');
$b = new Foo('123');
if ($a->equals($b)) { // should be true
// do something
}
Don't use public static properties and constants, global variables e.t.c.
Examples:
- Bad
class Logger
{
public function logWarning(string $message)
{
$this->loggerService->warning($message . PHP_EOL); // global constant PHP_EOL
}
}
- Better
final class EOLString implements StringType
{
public function __construct(
private readonly StringType $string
) {}
public function value(): StringType
{
return $this->string . PHP_EOL; // use this legacy in only one place
}
}
final class WarningLog implements Log
{
public function __construct(
private readonly LoggingService $service,
private readonly EOLString $message
) {}
public function log(): void
{
$this->service->warning($this->message);
}
}
Act as if NULL did not exist.
Examples:
- Bad
class User
{
private string $sessionId;
private ?string $name = null;
public function authorize($name)
{
if ($name === null) {
throw new Exception();
}
$this->name = $name;
}
}
- Better
final class Guest implements User
{
public function __construct(
private StringType $sessionId
) {}
}
final class AuthorizedUser implements User
{
public function __construct(
private StringType $sessionId,
private StringType $name
) {}
}
Examples:
- Bad
class User
{
private User $instance;
private function __construct() {}
public static function getInstance(): User
{
if ($this->instance === null) {
$this->instance = new static();
}
return $this->instance;
}
}
- Better
final class AuthorizedUser implements User
{
// encapsulation
}
final class ReportPage implements Page
{
public function __construct(
private readonly User $user,
private readonly ReportPageTemplate $template,
) { }
public function html(): Html
{
return $this->template->parsedTemplate($this->user);
}
}
Examples:
- Bad
class User
{
private string $name;
private string $phone;
private string $address;
public function getName(): string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getPhone(): string
{
return $this->phone;
}
public function setPhone(string $phone): static
{
$this->phone = $phone;
return $this;
}
public function getAddress(): string
{
return $this->address;
}
public function setAddress(string $address): static
{
$this->address = $address;
return $this;
}
}
- Better
final class AuthorizedUser implements User
{
public function __construct(
private UserName $name,
private UserPhone $phone,
private UserAddress $address
) {}
public function changePhone(UserPhone $phone): static
{
return new static($this->name, $phone, $this->address);
}
public function address(): UserAddress
{
return $this->address;
}
public function printWith(Template $template): void
{
return $template
->with('name', $this->name)
->with('phone', $this->phone)
->with('address', $this->address)
->print();
}
}
Avoid any reflections.
Examples:
- Bad
class Foo
{
public function getQuuxByObject($object): Quux
{
if ($object instanceof Bar || $object::class === '\Baz') {
return $this->secondQuux();
}
return $this->firstQuux();
}
}
- Better
interface Foo
{
public function quuxForBar(): Quux;
public function quuxForBaz(): Quux;
}
Don't use private constants and static variables, use encapsulation or dependency injection or uniqueness.
Examples:
- Bad
final class DefaultOrder implements Order
{
private static $errorMessage = 'something wrong';
public function sell(): void
{
if (true) { // something wrong
throw new Exception(static::$errotMessage);
}
// sell
}
public function decline(): void
{
if (true) { // something wrong
throw new Exception(static::$errotMessage);
}
// decline
}
}
- Better
final class DefaultOrder implements Order
{
/**
* @throws Exception
*/
public function sell(): void
{
if (true) { // something wrong
throw new Exception("something wrong - can't sell");
}
// sell
}
/**
* @throws Exception
*/
public function decline(): void
{
if (true) { // something wrong
throw new Exception("something wrong - can't decline");
}
// decline
}
}
Don't send parameters to objects to configure their behavior.
Examples:
- Bad
final class DefaultBook implements Book
{
public function __construct(
private readonly LogStream $stream,
private readonly string $title,
private readonly bool $loggable
) {}
public function sell()
{
// sell
if ($this->loggable) {
$this->stream->add('book sold');
}
}
}
- Better
final class DefaultBook implements Book
{
public function __construct(
private readonly BookTitle $title
) {}
public function sell()
{
// sell
}
}
final class LoggableBook implements Book
{
public function __construct(
private readonly LogStream $stream,
private readonly DefaultBook $book
) {}
public function sell()
{
$this->book->sell();
$this->stream->add('book sold');
}
}
Don't use native or DocBlock annotations if it needs a reflection.
Examples:
- Bad
class BlogController extends AbstractController
{
#[Route('/blog', name: 'blog_list')]
public function list(): Response
{
// ...
}
}
- Better
class BlogList
{
public function html(): Html
{
//
}
}
Throw exceptions as soon as possible and as many as possible.
Examples:
- Bad
class FileHelper
{
public function list(string $dir): array
{
// there is no exception if there are no files in the directory, it is "fail safe" strategy
$files = $this->listFiles($dir);
if ($files === null) {
throw new FileException(); // the worst thing is to ignore it
}
return $this->parseFileNames($dir);
}
}
- Better
interface Files
{
public function exists(): bool;
public function names(): FileNames;
}
final class FilesPage implements WebPage
{
public function __construct(
private readonly Files $files,
private readonly FilesTemplate $template
) {}
public function html(): Html
{
return $this->template->renderedHtml($this->files->names());
}
}
final class StrictFilesPage implements WebPage
{
public function __construct(
private readonly Files $files,
private readonly FilesPage $page,
) {}
/**
* @throws EmptyDirectoryException
*/
public function html(): Html
{
// it's even better design it like "return new ThrowableIf(...)", the lines below are coupled
if (!$this->files->exists()) {
throw new EmptyDirectoryException();
}
return $this->page->html();
}
}
Use DocBlock to declare exceptions and keep it up to date.
Examples:
- Bad
class Foo
{
public function bar($filePath)
{
if (!file_exists($file_path)) {
throw new FilePathException();
}
$this->handle();
}
}
- Better
final class PostComment implements Comment
{
public function __construct(
private readonly Message $message,
private readonly File $file
) {}
/**
* @throws FileNotFoundException
* @throws FileNotLegalException
*/
public function withFile(File $file): Comment
{
// it's even better wrap all cases to other objects (decorators)
if (!$file->exists()) {
throw new FileNotFoundException();
}
if (!$file->legal()) {
throw new FileNotLegalException();
}
return new static($this->message, $file);
}
}
Examples:
- Bad
class File
{
public function getContent(): int
{
try {
return $this
->getFile($this->path)
->getContent();
} catch (FileException $e) {
return 0;
}
}
}
- Better
interface File
{
public function asString(): StringType;
}
final class FilePage implements WebPage
{
public function __construct(
private readonly File $file,
private readonly FileTemplate $template
) {}
public function html(): StringType
{
try {
return $this->template->renderedString($this->file->asString());
} catch (FileException $exception) {
return $this->template->failedTemplate($exception);
}
}
}
Rethrow exceptions everywhere you catch them and chain a new exception with previous.
Examples:
- Bad
class Files
{
/**
* @throws FilesException
*/
public function countLegal(): int
{
$legalFiles = 0;
try {
foreach($this->files as $file) {
if ($file->isLegal()) {
$legalFiles++;
}
}
} catch (FileException $exception) {
throw new FilesException("can't count legal files");
}
return $legalFiles;
}
}
- Better
final class LegalFiles implements Files
{
/**
* @throws LegalFilesException
*/
public function sendToEmails(Emails $emails): void
{
try {
// it's better to create an objective mapping: new Map($this->files)
array_map(fn($file) => $this->mail->add($emails, $file), $this->files);
} catch (FilesException $exception) {
throw new LegalFilesException(
message: "Can't send to emails",
previous: $exception
);
}
}
}
Examples:
- Bad
class File
{
public function getContent(): int
{
try {
return $this
->getFile($this->path)
->getContent();
} catch (FileException $e) {
return 0;
}
}
}
- Better
final class WebApplication implements Application
{
public function run(): void
{
try {
echo $this->response(
$this->templates->templateForRoute(
$this->route->matched()
)
);
} catch (Exception $exception) {
echo $this->response($this->templates->failedTemplate());
}
}
}
Use destructors and destroy objects when you need to.
Create objects of other interfaces only in the object constructor with a condition.
It is better to create objects of other interfaces in secondary constructors, which is not yet available in PHP.
Examples:
- Bad
class Handler
{
public function handle(RequestInterface $request): ResponseInterface
{
$helper = new Helper();
$response = new Response();
if ($helper->helpNeeded()) {
$response->setSomething($helper->something());
}
return $response;
}
}
- Better
final class MyRule implements Rule
{
private Foo $foo;
public function __construct(ArrayType $instances) // you can design it better
{
if (array_key_exists('foo', $instances)) {
$this->foo = $instances['foo'];
}
$this->foo = new Foo();
}
public function matches(): bool
{
return $this->foo->someExpected();
}
}
Don't use the fluent interface pattern if the object just returns another object, only use it if the object creates another object.
Examples:
- Bad
final class DefaultFoo implements Foo
{
public function bar(): Bar
{
return $this->bar;
}
}
final class DefaultBar implements Bar
{
public function baz(): void
{
$this->baz();
}
}
$foo = new DefaultFoo();
$foo->bar()->baz();
- Better
final class DefaultFoo implements Foo
{
public function __construct(
private readonly Bar $bar
) { }
public function bar(): Bar
{
return $this->bar->withQuux(); // creation
}
}
final class DefaultBar implements Bar
{
public function withQuux(): static
{
return new static($this->quux);
}
public function baz(): void
{
$this->baz();
}
}
$foo = new DefaultFoo();
$foo->bar()->baz();
It is acceptable to use Aspects in annotations if the AOP functionality is not based on reflection.
Examples:
- Bad
class Parser
{
public function parse(): void
{
$attempts = 0;
while (true) {
try {
$attempts++;
$this->service->parse();
} catch (Exception $e) {
if ($attempts > 2) {
throw $e;
}
}
}
}
}
- Better
final class FooAPI implements API
{
#[RetryOnFailure(attempts: 5)]
public function response(): Response
{
return $this->adapter->resposeForRequest($this->request);
}
}
- Without AOP
final class FooAPI implements API
{
public function response(): Response
{
return $this->adapter->resposeForRequest($this->request);
}
}
final class FooAPIWithAttempts implements API
{
public function __construct(
private readonly API $api
) { }
/**
* @return Response
*
* @throws Exception
*/
public function response(): Response
{
// it's even better design it like "return new ObjectiveWhile(...)" or something else, because the lines below are coupled
$attempts = 0;
while (true) {
try {
$attempts++;
return $this->api->response();
} catch (Exception $e) {
if ($attempts > 2) {
throw $e;
}
}
}
}
}
Use decorators for validation and different results (as objects) for assertions.
Examples:
- Bad
interface FormValidator
{
public function validate($fileds);
}
- Better
interface FooReport
{
public function asArrayBetweenDates(DateTime $from, DateTime $to): ArrayType;
}
final class FooSQLReport implements FooReport
{
public function __construct(
private readonly DbConnection $connection,
private readonly TableName $tableName
) { }
public function asArrayBetweenDates(DateTime $from, DateTime $to): ArrayType
{
return $this->connection->sqlResultAsArray(
sql: 'SELECT * FROM ?s ...',
tableName: this->tableName->value(),
);
}
}
final class StrictFooSQLReport implements FooReport
{
public function __construct(
private readonly FooSqlReport $report
) { }
/**
* @param DateTime $from
* @param DateTime $to
* @return ArrayType
*
* @throws Exception
*/
public function asArrayBetweenDates(DateTime $from, DateTime $to): ArrayType
{
if ($from < new DateTime('now')) {
throw new InvalidDateException();
}
return $this->report->asArrayBetweenDates($from, $to);
}
}