2. Petr Heinz
More than 8 years of programming experience.
Loves clean code, regular expressions and
clever design.
Dedicated last year to developing the Shopsys
Framework, open source e-commerce
framework made with passion on Symfony 3.
✉ petr.heinz@shopsys.com
4. Create, Read, Update and Delete
Four basic functions of an persistent storage, often used as an API.
Can be mapped to SQL statements:
INSERT SELECT UPDATE DELETE
Can be mapped to HTTP methods (used in REST APIs):
PUT GET POST DELETE
5. Example: Article
class Article {
private $author, $text, $state, $published;
public function setAuthor(Author $author) {
$this->author = $author;
}
public function getAuthor() {
return $this->author;
}
public function setText($text) {
$this->text = $text;
}
// ...
}
class ArticleController {
public function create(...) {
// ...
}
public function update(...) {
// ...
$article->setAuthor($author);
$article->setText($text);
$article->setState($state);
$article->setPublished($published);
}
public function delete(...) {
// ...
}
}
6. Entities Must Follow Business Rules
Entities are often constrained by business rules and the consistency must be kept.
● Company customer must have VAT ID filled in his account.
● Sold out product must be hidden.
● Article in published state must have published date.
This is difficult to achieve in the previous example. That’s because all article
attributes can be changed independently. Developers are not restricted in the way
they interact with the object.
7. Example: Article without setters
class Article {
const STATE_UNPUBLISHED = 1;
const STATE_PUBLISHED = 2;
const STATE_DELETED = 3;
private $author, $text, $state, $published;
public function __construct(Author $author, $text, $state, DateTime $published = null) {
// ...
}
public function update(Author $author, $text, $state, DateTime $published = null) {
// ...
}
public function delete() {
$this->state = self::STATE_DELETED;
}
}
8. Example: Article without setters
class Article {
// ...
public function update(Author $author, $text, $state, DateTime $published = null) {
if ($this->state === self::STATE_DELETED) {
throw new ArticleIsDeletedException($this);
}
$this->author = $author;
$this->text = $text;
$this->state = $state;
if ($state === self::STATE_PUBLISHED) {
$this->published = $published ?: new DateTime();
} elseif ($state === self::STATE_UNPUBLISHED) {
$this->published = null;
}
}
}
10. Object Oriented Programming
Objects have both data (their properties) and behavior (their methods).
Objects model real-world behavior, concepts and relationships.
Encapsulation principle tells us to hide the details about the data and focus solely
on the behavior - the public methods of our objects (“Tell, Don’t Ask”).
In PHP it is easy to combine procedural and object-oriented programming.
11. Example: Bank Account Object
class BankAccount {
private $balance;
public function __construct(Money $balance) {
$this->balance = $balance;
}
public function deposit(Money $amount) {
$this->balance = $this->balance->add($amount);
}
public function withdraw(Money $amount) {
if ($this->balance->isLessThan($amount)) {
throw new InsufficientFundsException($balance, $amount);
}
$this->balance = $this->balance->subtract($amount);
}
}
13. Let’s Define Some Terms First
Domain: Most common definition: “A sphere of knowledge or activity.”
It’s basically the subject area of your application (eg. an online store or news site).
Domain Model: System of abstractions describing part of the domain that can be
used to solve problems. Simplification of the real world.
Domain Object: Object that is part of the domain model (eg. Product, Order, …).
Business Logic: High-level logic reflecting the real-world business rules.
14. Anemic Domain Model
No business logic in domain objects
Clearly separates logic and data
Violates object encapsulation
Works well for simple applications
Leads to procedural programming
Called an anti-pattern by M. Fowler
Business logic mainly in domain objects
Domain objects encapsulate inner data,
offer meaningful behavior
Data integrity kept by the encapsulation
Better choice for complex domain
models
Rich Domain Model
15. Anemic Domain Model Rich Domain Model
class Worker {
function getVelocity() {
return $this->velocity;
}
function setVelocity($velocity) {
$this->velocity = $velocity;
}
}
class WorkerService {
function work(Worker $w, Task $t, $time) {
$progress = $t->getProgress();
$progress += $w->getVelocity() * $time;
$t->setProgress($progress);
}
}
class Worker {
function __construct($velocity) {
$this->velocity = $velocity;
}
function work(Task $task, $time) {
$progress = $this->velocity * $time;
$task->makeProgress($progress);
}
}
17. Setters Do Not Exist in the Real World
Setters have no meaning in the real world:
● A writer does not set a “published” state to an article, he/she publishes it.
● Customers do not set a “paid” status to an order, they pay for it.
● Your happy boss does not set a higher salary to you, he/she raises it.
There is always a better, more expressive, alternative to a setter.
Expressive statements lead to more readable code.
18. Nobody Expects The Setters To Do Stuff
Similarly to adding logic to a CRUD
update, you might feel the need to add
some business logic to your setter.
The problem with this is that nobody
expects setters to do anything beside
setting the property.
An unexpected behavior leads to bugs.
class Order {
// ...
public function setStatus($status) {
if (!$this->isValidStatus($status)) {
throw new InvalidArgumentException();
}
$this->status = $status;
if ($status === self::STATUS_PAID) {
$this->mailService->sendOrderPaidMail(
$this->orderNumber, $this->customer
);
}
}
}
19. Nobody Expects The Setters To Do Stuff
Similarly to adding logic to a CRUD
update, you might feel the need to add
some business logic to your setter.
The problem with this is that nobody
expects setters to do anything beside
setting the property.
An unexpected behavior leads to bugs.
class Order {
// ...
public function pay() {
$this->status = self::STATUS_PAID;
$this->mailService->sendOrderPaidMail(
$this->orderNumber, $this->customer
);
}
public function cancel() {
$this->status = self::STATUS_CANCELLED;
}
}
20. An Update in CRUD Is Similar to a Setter
Generic update method in CRUD is similar to a setter:
● It does not have a real-world meaning.
● There are better alternatives based on real scenarios to be implemented.
For example, by “updating” an article we mean “rewriting” it and possibly
“publishing”, “unpublishing” or “deleting” it.
22. Focus on Real Scenarios
By building your application around you domain objects and their behavior you
can get expressive code that is easier to understand, use and maintain.
Concept of “setting” or “updating” to too generic to be meaningful.
Your API should be focused on real scenarios, real use-cases. This will keep the
API clean and intuitive and it will help you keep the integrity of your data.
23. Think About the Way You Program
There is no best way, no silver bullet. And there probably never will be one.
Keep in mind the techniques of object-oriented programming, encapsulation
principle, focusing on the behavior.
Knowing about the two extremes will help you improve the design of your
application and choose the proper solution for your project.
24. Need CRUD methods? Add a Layer.
If you for some reason want to
allow classical CRUD methods,
you can build it on top of your
internal API.
You can use Adapter pattern for
this task.
class Article {
// ...
public function update(...) {
if ($this->state === self::STATE_DELETED) {
throw new ArticleIsDeletedEx($this);
}
$this->author = $author;
$this->text = $text;
$this->state = $state;
if ($state === self::STATE_PUBLISHED) {
$this->published = $published ?: new DateTime();
} elseif ($state === self::STATE_UNPUBLISHED) {
$this->published = null;
}
}
}
25. Need CRUD methods? Add a Layer.
If you for some reason want to
allow classical CRUD methods,
you can build it on top of your
internal API.
You can use Adapter pattern for
this task.
class ArticleCrudAdapter {
// ...
public function update(...) {
if ($this->article->isDeleted()) {
throw new ArticleIsDeletedEx($this->article);
}
$this->article->rewrite($text, $author);
switch ($state) {
case Article::STATE_PUBLISHED:
$this->article->publish($published); break;
case Article::STATE_UNPUBLISHED:
$this->article->unpublish(); break;
case Article::STATE_DELETED:
$this->article->delete(); break;
}
}
}
26. It Is About the Balance
Every way has its tradeoffs and, as always, it is about the balance.
Focusing on real-world use-cases helps to maintain integrity and usability.
Integration with other libraries or components is easier with generic methods.
When developing Shopsys Framework we try to keep that in mind, and take
inspiration from Rich Domain Model.
See for yourself, join closed beta: https://www.shopsys-framework.com