Implementing domain-specific design in PHP

Original author: Alireza Rahmani Khalili
  • Transfer
Hello again!

Well, the next "new" course, which started at the end of December, is coming to an end - "Backend developer in PHP . " We took into account various small roughnesses and launch a new one. It remains only to look at the release and that's it, put another checkmark.

For now, let's look at one interesting article.

Go.

In this article, you will learn how to use PHP to manage your company's next DDD project and effectively model real-world situations to help define your business logic.

Subject-oriented design (Domain-Driven Design, hereinafter referred to as DDD) is a software development methodology for designing complex software projects with the goal of delivering an end product that meets the organization's objectives. In fact, DDD helps focus the project on an evolving core 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 refers. I’ll try to explain in more detail. "Region" 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 you need to implement in your project.

The domain model is your structured solution to the problem. It should be a dictionary and key concepts of tasks from the subject area.

Single language


“Ubiquitous Language” is a 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:

//add is a static method in product class
$product = Product::add(
    new Title('Mobile Phone'),
    new Price('1000')
);

If someone creates a product in the development team and someone adds, this violates the common language. In this case, if we have additional actions in the method of adding a product, such as sending emails, all of them will be skipped, and the definition of adding a product to the team will also be changed. We would have two different definitions for one term.

Layered architecture


In this article I am not going to talk about object-oriented design. But DDD assumes the basics of good design. Eric Evans (author of the book “Object-Oriented Design (DDD). Structuring Complex Software Systems”) believes that developing a good domain model is an art.

To develop a good domain model, you need to know about Model-Driven Design. Model-Driven Design - a combination of model and implementation. Multilayer architecture is one of its blocks.

Layered Architecture is the idea of ​​isolating every part, based on many years of experience and developer collaboration. The 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 layer of a user interface (presentation). The application layer is a way of communicating 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, the controllers are located here.

The 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 level provides general technical capabilities that support higher levels, and also supports the structure of interactions between the four layers (this is why the repositories are in this layer).

Linking between 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 lower layers should connect to the upper layer, they should use patterns such as Callback or Observer.

Value Objects and Entities


I am 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 a lot of different ways of describing value objects from different points of view. Fortunately, each of the different explanations helped me deepen my understanding of value objects rather than contradict each other.

Value objects are accessible by their value, not by 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 like 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, the email address may be a string, or it may 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 that are accessible by identifiers in our application. In fact, an entity is a collection of properties that have a unique identifier. A good example is a series of database tables. An entity is volatile because it can change its attributes (usually with setters and getters) and also has a life cycle, that is, it can be deleted.

Is an object something with continuity and identity - something that is tracked in different states or even in different implementations? Or is it an attribute describing the state of something else? This is the main difference between an entity and a value object.

Aggregates


A model can contain a large number of domain objects. No matter how much we foresee when modeling a region, it often happens that many objects depend 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 reasonably talk about your code.

Aggregates help reduce the number of bidirectional associations between objects in the system, because you are allowed to store links only to the root. This greatly simplifies the design and reduces the number of blind changes in the graph of objects. On the other hand, aggregates help with the decoupling of large structures, 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 global identity and is ultimately responsible for checking invariants.
  • Root objects have global identity. Internal entities have a local identity that is unique only within the aggregate.
  • Nothing outside the aggregate boundary can contain a link to anything inside except the root object. A root object can associate links with internal objects to other objects, but these objects can only use them temporarily and cannot be attached to a 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 links to other aggregate roots.
  • The delete operation should delete everything within the common border at once (garbage collection is easy. Since there are no external links to anything except the root, delete the root and everything else will be collected).
  • When a change is made to an object at the boundary of the aggregate, all invariants of the aggregate must be satisfied.

Factories


In the OOP world, 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 the aggregate, which provides the factory method for creating instances of an aggregate of a different type (or internal parts), will be primarily responsible for ensuring its main aggregate behavior, and the factory method is just one of them. Factories can also provide an important level of abstraction that protects the client from being dependent on a particular class.

There are times when a factory is not needed, and a simple designer 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 facilities


A repository is a layer that sits between the domain of your project and the database. Martin Fowler writes in his book Enterprise Application Templates that storage is an intermediate interaction between a domain and a data mapping layer using an interface similar to a collection to access domain objects.
This means that you should think of accessing data in your database as well as standard collection objects.

Let me explain a little more. Imagine that in DDD you may need to create an object either using a constructor or using a factory; You must request it from the root of the aggregate. 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 necessary object. This will lead to the creation of a number of associations by developers that are not really needed.

Another reason storage is important is access to the database. The programmer does not need to know the details necessary to access it. Because the database is at the infrastructure level, it has to deal with a lot of infrastructure details, not domain concepts. In addition, if the developer requests the launch of the request, this will lead to the disclosure of even more internal details necessary for the request.

If we do not have storage, the domain focus 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 is moved to the queries and developer code, so aggregates will be useless.

Finally, the vault acts as a storage location for objects available around the world. Storage may also include a strategy. It can access one persistent storage or another based on the specified strategy.

Implementation in Laravel


As you may already know, the best choice for implementing DDD in PHP is Doctrine ORM. To implement aggregates and storages, 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 change 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 a basic repository at the domain level, a basic repository derived from the Doctrine EntityRepository, which will allow us to have all the built-in Doctrine repository functions. Here we can also use our common functionality, and all our repositories should inherit from it. The implementation is as follows:

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 page repository, and the second is the comment repository. All stores must have an entityClass property to define an entity class. In this case, we can encapsulate the (private property) object in our repository:

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;
/**
 * Pages
 *
 * @ORM\Table(name="pages")
 * @ORM\Entity
 */
class Pages
{
    /**
     * @var string
     *
     * @ORM\Column(name="page_title", type="string", length=150, nullable=false)
     */
    private $pageTitle;
    /**
     * @ORM\OneToMany(targetEntity="App\Domain\User\Comments\Model\Entities\Comments", mappedBy="pageId", indexBy="pageId", cascade={"persist", "remove"})
     */
    private $pageComment;
    /**
     * @var integer
     *
     * @ORM\Column(name="page_id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $pageId;
    public function __construct()
    {
        $this->pageComment = new ArrayCollection();
    }
    /**
     * @param Comment
     * @return void
     */
    public function addComments(Comments $comment)
    {
        $this->pageComment[] = $comment;
    }
//... other setters and getters.
}
namespace App\Domain\User\Comments\Model\Entities;
use Doctrine\ORM\Mapping as ORM;
/**
 * Comments
 *
 * @ORM\Table(name="comments")
 * @ORM\Entity
 */
class Comments
{
    /**
     * @ORM\ManyToOne(targetEntity="App\Domain\User\Core\Model\Entities\Users")
     * @ORM\JoinColumn(name="users_user_id", referencedColumnName="id")
     */
    private $usersUserId;
    /**
     * @ORM\ManyToOne(targetEntity="comments", inversedBy="children")
     * @ORM\JoinColumn(name="parent_id", referencedColumnName="comment_id")
     */
    private $parentId;
    /**
     * @ORM\ManyToOne(targetEntity="App\Domain\Interactions\Core\Model\Entities\pages", inversedBy="pageComment" )
     * @ORM\JoinColumn(name="page_id", referencedColumnName="page_id")
     */
    private $pageId;
    /**
     * @ORM\OneToMany(targetEntity="comments", mappedBy="parent")
     */
    private $children;
    /**
     * @param Page
     * @return void
     */
    public function __construct()
    {
        $this->children = new\Doctrine\Common\Collections\ArrayCollection();
    }
}

As you can see, in the above code I define relationships in object annotations. Implementing relationships in Doctrine ORM may seem very complicated, but it's actually not that difficult when you get to know how things work. The only way to add comments is to call the addComments of the page object, and this method accepts only the comment entity object as input. This will make us confident in the functionality of our code.

My aggregate 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 PageId is valid; I mean that without PageId access to comments is not possible. Say comments without a valid page id are meaningless and inaccessible. In addition, there is a comment factory 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 have identified two factories. The first is the comment type, 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)
    {
        // TODO: Implement choisir() method.
        if (is_null($administrative)) {
            $comment = new Comment($this->page, $this->comment);
            return $comment->confectionner();
        }
        $comment = new AdministrativeComments($this->page, $this->comment, $this->parentId);
        return $comment->confectionner();
    }
}

The Comment factory method provides the internal parts of the aggregate.

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

As you can see in the above code, we have two classes: Comment and AdministrativeComments. Commentfactory will decide which class to use. Some unnecessary classes or methods, such as the Authorization class and reject method, are omitted here. As you can see in the above code, I use RequestFactory for validation. This is another factory method in our application that is responsible for validating input. This type of validation has a definition in DDD, and is also added in laravel 5+.

Conclusion


It will take many articles to cover all of these definitions, but I did my best to summarize them. It was just a simple example of an aggregate root, but you can create your own complex aggregate, and I hope this example helps you.

THE END

As always, we are waiting for comments, questions here or at our Open Day.

Also popular now: