Hello again!
Well, the next “new” course, which started at the end of December, is coming to an end -
“Backend developer in PHP” . Took into account various small roughness and launch a new one. It remains only to look at the issue and everything, put another tick.
But wait a minute, let's look at one interesting article.
')
Go.
In this article, you will learn how to use PHP to manage the next DDD project of your company and effectively model real-world situations to help define your business logic.
Domain-driven design (Domain-Driven Design, hereinafter - DDD) is a software development methodology for designing complex software projects with the aim of delivering the final product that meets the objectives of the organization. In fact, DDD helps to focus the project on the evolving base model.
DDD will teach you how to effectively model the real world in your application and use OOP to encapsulate the organization's business logic.

What is a domain model?
In my opinion, the Domain Model is your perception of the context to which it relates. I will try to explain in more detail. "Area" in itself means the world of business with which you work, and the tasks that it is intended to solve. For example, if you want to develop an application for online food delivery, everything in your subject area (tasks, business rules, etc.) will be about online food delivery that needs to be implemented in your project.
The domain model is your structured problem solution. It should be a dictionary and key concepts of tasks from the subject area.
Common language
“Unified Language” (“Ubiquitous Language”) is the language used by business professionals to describe a domain model. This means that the development team consistently uses this language in all interactions and in the code. The language should be based on a region model. Let me give an example:
$product = new Entity\Product(); $product->setTitle(new Title('Mobile Phone')); $product->setPrice(new Price('1000')); $this->em->persist($product); $this->em->flush();
In the code above, I create a new product, but in the application the product must be added, not created:
If in the development team, someone creates a product, and someone adds, it violates a single language. In this case, if we have additional actions in the method of adding a product, such as sending emails, they will all be skipped and the definition of adding a product to the team will be changed. We would have two different definitions for the same term.
Layered architecture
In this article, I'm not going to talk about object-oriented design. But DDD involves the basics of good design. Eric Evans (Eric Evans - author of the book “Domain-Oriented Design (DDD). Structuring Complex Software Systems”) believes that the development of a good domain model is an art.
To develop a good domain model, you need to know about Model-Driven Design. Model-Driven Design - combining model and implementation. Layered architecture is one of its blocks.
Layered Architecture is the idea of isolating each part based on many years of experience and collaboration from developers. Layers are listed below:
- User interface
- Application level
- Domain Level
- Infrastructure level
The user interface is responsible for displaying information to the user and interpreting his commands. In Laravel, a mapping is a user interface layer (presentation). The application level is a way to communicate with the outside world (outside the domain). This layer behaves like an open API for our application. It does not contain business rules or knowledge. In Laravel controllers are located right here.
Domain level is the heart of business software. This level is responsible for presenting business concepts, information about the business situation and business rules. The infrastructure layer provides general technical capabilities that support higher layers and also supports the structure of the interactions between the four layers (which is why the repositories are in this layer).
The connection between the layers is mandatory, but without losing the benefits of separation. Communication takes place in one direction. As can be seen from the diagram above, the upper layers can interact with lower levels. If the bottom layers are to be connected to the top layer, they should use patterns such as Callback or Observer.
Value Objects and Entities
I'm a big fan of Value Objects. I think they are the essence of OOP. Although DDD value objects seem simple, they are a serious source of confusion for many, including me. I read and heard many different ways of describing value objects from different points of view. Fortunately, each of the different explanations rather helped me to deepen the understanding of object values, rather than contradict each other.
Value objects are accessible by their value, not by their identifier. These are immutable objects. Their values do not change (or change, but rarely) and do not have a life cycle (this means that they are not as rows of database tables that can be deleted), for example, currencies, dates, countries, etc.
You can create value objects that you do not recognize as value objects. For example, an email address can be a string, or it can be a value object with its own set of behaviors.
The code below shows an example of a value object class:
final class ImagesTypeValueObject { private $imageType; private $validImageType = ['JPEG', 'GIF', 'BMP', 'TIFF', 'PNG']; public function __construct($imageType) { Assertion::inArray($this->validImageType, $imageType, 'Sorry The entry is wrong please enter valid image type'); $this->imageType = $imageType; } public function __toString() { return $this->imageType; } }
Entities are objects accessible by identifiers in our application. In fact, an entity is a set of properties that have a unique identifier. A good example is the number of database tables. An entity is changeable, because it can change its attributes (usually with the help of setters and getters), and also has a life cycle, that is, it can be deleted.
The object is something with continuity and identity - something that is tracked in different states or even in different implementations? Or is it an attribute that describes the state of something else? This is the main difference between an entity and a value object.
Aggregates
A model may contain a large number of domain objects. Regardless of how much we foresee when modeling a region, it often happens that many objects are dependent on each other, creating a set of relationships, and you cannot be 100% sure of the result. In other words, you should be aware of the business rule that must always be followed in your domain model; Only with this knowledge can you talk about your code with confidence.
Aggregates help reduce the number of bidirectional associations between objects in the system, because you are allowed to store references only to the root. This greatly simplifies the design and reduces the number of blind changes in the object graph. On the other hand, aggregates help with the decoupling of large structures by establishing the rules of relations between entities. (Note: aggregates can also have properties, methods and invariants that do not fit into one class)
Eric Evans, in his book, set some rules for implementing aggregates, and I list them below:
- The root object has a global identity and is ultimately responsible for checking invariants.
- Root objects have a global identity. Internal entities have a local identity, unique only within the aggregate.
- Nothing outside the boundaries of an aggregate can contain a link to anything inside but the root object. The root object can associate links with internal objects to other objects, but these objects can only use them temporarily and cannot be attached to the link.
- As a consequence of the above rule, only basic roots can be obtained directly with database queries. All other objects must be found by traversing associations.
- Objects within an aggregate may contain references to other aggregate roots.
- The delete operation should immediately delete everything within the common boundary (with garbage collection it is easy. Since there are no external links to anything other than the root, delete the root and everything else will be collected).
- When a change is made on an object at the boundary of the aggregate, all invariants of the aggregate must be satisfied.
Factories
In the OOP world, a Factory is an object that is only responsible for creating other objects. In DDD, factories are used to encapsulate the knowledge needed to create objects, and they are especially useful for creating aggregates.
The root of an aggregate that provides a factory method for creating instances of an aggregate of a different type (or internal parts) will have primary responsibility for ensuring its basic aggregating behavior, and the factory method is just one of them. Factories can also provide an important level of abstraction that protects the client from dependence on a particular class.
There are cases when the factory is not needed, and a simple constructor is enough. Use the constructor when:
- The design is not complicated.
- Creating an object is not related to creating others, and all the necessary attributes are passed through the constructor.
- The developer is interested in implementation and may want to choose a strategy to use.
- Class - type. There is no hierarchy, so there is no need to choose between a list of specific implementations.
Storage
Storage is the layer that lies between the domain of your project and the database. Martin Fowler, in his book Corporate Application Templates, writes that storage is an intermediate interaction between a domain and a data mapping layer using an interface like a collection for accessing domain objects.
This means that you should think about accessing the data in your database as well as the standard collection objects.
Let me explain in a bit more detail. Imagine that in DDD you may need to create an object either with a constructor or with a factory; you must request it from the root of the unit. The problem is that the developer must have a link to the root. For large applications, this becomes a problem, because you need to make sure that developers always have a link to the required object. This will lead to the creation by the developers of a number of associations that are not really needed.
Another reason why storage is important is access to the database. The programmer does not need to know the details needed to access it. Because the database is at the infrastructure level, it has to deal with a lot of the details of the infrastructure, and not with the concepts of the domain. In addition, if the developer requests the launch of the request, it will lead to the disclosure of even more internal parts required by the request.
If we do not have storage, the focus of the domain will be lost, and the design will be compromised. Therefore, if developers use queries to access data from the database or pull out several specific objects, the domain logic moves into queries and developer code, so the aggregates will be useless.
Finally, the repository acts as a storage place for objects available worldwide. Storage may also include a strategy. It can access one persistent storage or another based on a specified strategy.
Implementation in Laravel
As you might know, the best choice for implementing DDD in PHP is Doctrine ORM. To implement aggregates and storage, we need to make some changes to our entities and create some files at our domain level.
I decided to implement a small part of the application in which the user can create a page or edit it. Each page may contain many comments, and each comment may contain some sub-comments. Administrators can approve / reject comments after adding them.
In the above scenario, the first step is to create the base storage in the domain level, the base storage obtained from the Doctrine EntityRepository, which will allow us to have all the built-in functions of the Doctrine storage. Here we can also use our shared functionality, and all our repositories should inherit from it. The implementation looks like this:
namespace App\Domain\Repositories\Database\DoctrineORM; use App\Domain\Events\Doctrine\DoctrineEventSubscriber; use Doctrine\Common\Persistence\Event\LifecycleEventArgs; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\EntityManager; use GeneratedHydrator\Configuration; use Doctrine\Common\Collections\ArrayCollection; abstract class DoctrineBaseRepository extends EntityRepository { public $primaryKeyName; public $entityName = null; public function __construct(EntityManager $em) { parent::__construct($em, new ClassMetadata($this->entityClass)); $this->primaryKeyName = $em->getClassMetadata($this->entityClass)->getSingleIdentifierFieldName(); } }
We have two repositories. The first is the repository of pages, and the second is the repository of comments. All stores must have an entityClass property to define an entity class. In this case, we can encapsulate (the private property) object in our storage:
namespace App\Domain\Repositories\Database\DoctrineORM\Page; use App\Domain\User\Core\Model\Entities\Pages; use App\Domain\Repositories\Database\DoctrineORM\DoctrineBaseRepository; class DoctrinePageRepository extends DoctrineBaseRepository { private $entityClass = Pages::class; public function AddComments($pages) { $this->_em->merge($pages); $this->_em->flush(); } }
I use the Doctrine command line to generate entities:
namespace App\Domain\Interactions\Core\Model\Entities; use App\Domain\User\Comments\Model\Entities\Comments; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; class Pages { private $pageTitle; private $pageComment; private $pageId; public function __construct() { $this->pageComment = new ArrayCollection(); } public function addComments(Comments $comment) { $this->pageComment[] = $comment; }
As you can see, in the above code, I define relationships in the annotations of objects. Implementing relationships in Doctrine ORM may seem very difficult, but in fact it is not so difficult when you get to know how everything works. The only way to add comments is to call the addComments of the page object, and this method only accepts the comment entity object as input. This will make us confident in the functionality of our code.
My unit looks like this:
namespace App\Domain\Comment; use App\Domain\User\Comments\Model\Entities\Comments; use App\Domain\Repositories\Database\DoctrineORM\User\DoctrineCommentRepository; use App\Domain\Repositories\Database\DoctrineORM\Interactions\DoctrinePagesRepository; use Assert\Assertion; class PageAggregate { public $Pages; public $pageResult; public $parentId = null; public $comments; public $DoctrineRepository = DoctrinePagesRepository::class; public function __construct($id, $comments = null, $administrative = null) { $this->DoctrineRepository = \App::make($this->DoctrineRepository); Assertion::notNull($this->pageResult = $this->DoctrineRepository->findOneBy(['pageId' => $id]), 'sorry the valid page id is required here'); $commentFacory = new Commentfactory($this->pageResult, $comments); return $commentFacory->choisir($administrative); } }
We need an aggregate that is responsible for restricting access to comments if the PageId is valid; I mean that without PageId access to comments is impossible. Say, comments without a valid page id are meaningless and inaccessible. In addition, there is a factory comment method that helps us encapsulate business rules.
Factory Method:
namespace App\Domain\Comment; interface CommentTypeFactoryInterface { public function confectionner(); } namespace App\Domain\Comment; interface CommentFactoryInterface { public function choisir(); }
I identified two factories. The first is the type of comments, and the second is the comment interfaces, which make it mandatory for each comment when implementing the choisir method.
namespace App\Domain\Comment; use App\Application\Factory\Request\RequestFactory; class Commentfactory implements CommentFactoryInterface { private $page; private $comment; private $parentId; public function __construct($page, $comment = null) { $this->page = $page; $this->comment = $comment; } public function choisir($administrative = null) {
The Factory method Comment provides internal parts of the unit.
namespace App\Domain\Comment; use App\Domain\User\Comments\Model\Entities\Comments; use App\Domain\Repositories\Database\DoctrineORM\User\DoctrineCommentRepository; use App\Domain\Repositories\Database\DoctrineORM\Interactions\DoctrinePagesRepository; use App\Domain\Interactions\Core\Model\Entities\Pages; use App\Application\Factory\Request\RequestFactory; use Assert\Assertion; class Comment implements CommentTypeFactoryInterface { private $page; private $comments; public $DoctrineCommentRepository = DoctrineCommentRepository::class; public $DoctrineRepository = DoctrinePagesRepository::class; public function __construct(Pages $page, $comment) { $this->page = $page; $this->comments = $comment; $this->DoctrineCommentRepository = \App::make($this->DoctrineCommentRepository); $this->DoctrineRepository = \App::make($this->DoctrineRepository); } public function confectionner() { if (is_array($this->comments)) { \Request::replace($this->comments['data']); \App::make(RequestFactory::class); $this->addComments(); } elseif (is_null($this->comments)) { return $this->retrieveComments(); } elseif (is_int($this->comments)) { $this->deleteComment(); } return true; } private function addComments() { if (isset($this->comments['id']) && !is_null($this->comments['object'] = $this->DoctrineCommentRepository->findOneBy(['commentId' => $this->comments['id']]))) { return $this->editComment(); } $this->comments = $this->CommentObjectMapper(new Comments(), $this->comments['data']); $this->page->addComments($this->comments); $this->DoctrineRepository->AddComments($this->page); } private function editComment() { $comment = $this->CommentObjectMapper($this->comments['object'], $this->comments['data']); $this->page->addComments($comment); $this->DoctrineRepository->AddComments($this->page); } private function deleteComment() { $this->DoctrineCommentRepository->delComments($this->comments); } private function retrieveComments() { return $this->page->getPageComment(); }
namespace App\Domain\Comment; use App\Domain\Interactions\Core\Model\Entities\Pages; use App\Domain\Repositories\Database\DoctrineORM\User\DoctrineCommentRepository; use App\Domain\Repositories\Database\DoctrineORM\Interactions\DoctrinePagesRepository; use App\Domain\User\Comments; use Assert\Assertion; class AdministrativeComments implements CommentTypeFactoryInterface { private $page; private $comments; private $parentId; private $privilege; public $DoctrineCommentRepository = DoctrineCommentRepository::class; public $DoctrineRepository = DoctrinePagesRepository::class; public function __construct(Pages $page, $comment, $parentId) { $this->page = $page; $this->comments = $comment; $this->parentId = $parentId; $this->privilege = new Athurization(\Auth::gaurd('admin')->user()); } public function confectionner() { $action = $this->comments['action']; Assertion::notNull($this->comments = $this->DoctrineCommentRepository->findOneBy(['commentId' => $this->comments['id']]), 'no Valid comment Id'); $this->$action; return true; } public function approve() { $this->privilege->isAuthorize(__METHOD__); $this->DoctrineCommentRepository->approve($this->comments, \Auth::gaurd('admin')->user()); } public function reject() { $this->privilege->isAuthorize(__METHOD__); $this->DoctrineCommentRepository->reject($this->comments, \Auth::gaurd('admin')->user()); } public function delete() { $this->privilege->isAuthorize(__METHOD__); $this->DoctrineCommentRepository->delete($this->comments, \Auth::gaurd('admin')->user()); } }
, : Comment AdministrativeComments. Commentfactory , . , Authorization reject, . , RequestFactory . , . DDD, laravel 5+.
Conclusion
, , . , , , .
THE END
,
.