On Github Ocramius / doctrine-best-practices
A group of persistence-oriented libraries for PHP
(Must be prepared to take unpopular decisions)
That's what SQL is for
It's a QUERY language, after all
Entities should work without the ORM
Entities should work without the DB
class User { private $username; private $passwordHash; public function getUsername() :string { return $this->username; } public function setUsername(string $username) { $this->username = $username; } public function getPasswordHash() : string { return $this->passwordHash; } public function setPasswordHash(string $passwordHash) { $this->passwordHash = $passwordHash; } }
You can deal with state after designing the API
Designing State-first leads to terrible coupling
class User { private $banned; private $username; private $passwordHash; public function toNickname() : string { return $this->username; } public function authenticate( string $pass, callable $checkHash ) : bool { return $checkHash($pass, $this->passwordHash) && ! $this->hasActiveBans(); } public function changePass(string $pass, callable $hash) { $this->passwordHash = $hash($pass); } }
class User { // ... public function hasAccessTo(Resource $resource) : bool { return (bool) array_filter( $this->role->getAccessLevels(), function (AccessLevel $acl) use ($resource) : bool { return $acl->canAccess($resource) } ); } }
class User { // ... public function hasAccessTo(Resource $resource) : bool { return $this->role->allowsAccessTo($resource); } }
More expressive
Easier to test
Less coupling
More flexible
Easier to refactor
class User { private $bans; public function getBans() : Collection { return $this->bans; } }
public function banUser(Uuid $userId) { $user = $this->repository->find($userId); $user->getBans()->add(new Ban($user)); }
class User { private $bans; public function ban() { $this->bans[] = new Ban($this); } }
public function banUser(Uuid $userId) { $user = $this->repository->find($userId); $user->ban(); }
(You may need a DTO)
(Also applies to Temporary State)
(Regardless of the DB)
Named constructors are OK
class UserController { // form reads from/writes to user entity (bad) public function registerAction() { $this->userForm->bind(new User()); } }
class UserController { // coupling between form and user (bad) public function registerAction() { $this->em->persist(User::fromFormData($this->form)); } }
(For this use-case)
Use a DTO instead
Lifecycle Callbacks are supposed to be the ORM-specific serialize and unserialize
Don't use Lifecycle Callbacks for Business Logic/Events
Your db operations will block each other
You are denying bulk inserts
You cannot make multi-request transactions
Your object is invalid until saved
Your object does not work without the DB
Don't forget that a UUID is just a 128 bit integer!
public function __construct() { $this->id = Uuid::uuid4(); }
Are you looking for a DATETIME field instead?
You are just normalizing for the sake of it
Does your domain really NEED it?
Any reason to not use an unique constraint instead?
Do they make a difference in your domain?
Or append-only data-structures
class PrivateMessage { private $from; private $to; private $message; private $read = []; public function __construct( User $from, User $to, string $message ) { // ... } public function read(User $user) { $this->read[] = new MessageRead($user, $this); } }
class MessageRead { private $user; private $message; public function __construct(User $user, Message $message) { $this->id = Uuid::uuid4(); $this->user = $user; $this->message = $message; } }
Immutable data is simple
Immutable data is cacheable (forever)
Immutable data is predictable
Immutable data enables historical analysis
You may want to look at Event Sourcing...
Soft Deletes come from an era where keeping everything in a single DB was required
(and therefore validity)
Soft Deletes can usually be replaced with more specific domain concepts
We reach the limits of the ORM
Careful about performance/transaction size!
Code only what you need for your domain logic to work
Hack complex DQL queries instead of making them simpler with bi-directionality
final class UserRepository { public function findUsersThatHaveAMonthlySubscription() { // ... INSERT DQL/SQL HELL HERE ... } }
final class UsersThatHaveAMonthlySubscription { public function __construct(EntityManagerInterface $em) { // ... } public function __invoke() : Traversable { // ... INSERT DQL/SQL HELL HERE ... } }
So are Query Functions
Avoid ObjectManager#getRepository()
It is a ServiceLocator
It causes the same problems of the ServiceLocator
Separate MyRepository#get() and MyRepository#find()
MyRepository#find() can return null
MyRepository#get() cannot return null
final class BlogPostRepository { // ... public function getBySlug($slug) : BlogPost { $found = $this->findOneBy(['slug' => (string) $slug]); if (! $found) { throw BlogPostNotFoundException::bySlug($slug); } return $found; } }
Using a get() method that throws, you can simplify error logic
Use ObjectManager#clear() between different ObjectManager#flush() calls
Communicate between boundaries via identifiers, not object references
You may need to gag your DBAs...
Or get them to understand your needs
Academic and practical knowledge may differ
EntityManager
UnitOfWork
Metadata Drivers
DQL
Repositories
Second Level Cache
Measuring is the only way
There are other talks about this...
Otherwise, you're digging your own grave!
Transactional Data != Reporting Data