PHP Classes
PHP Classes
elePHPant
Icontem

PHP Domain Driven Design 2018 Tutorial with a Laravel Implementation

Recommend this page to a friend!
  Blog PHP Classes blog   RSS 1.0 feed RSS 2.0 feed   Blog PHP Domain Driven Des...   Post a comment Post a comment   See comments See comments (0)   Trackbacks (0)  

Author:

Posted on: 13 days ago

Categories: PHP Tutorials

Domain-Driven Design (also known as DDD) is a software development methodology for designing complex projects and deliver the final software product, so it meets the businesses goals.

DDD helps developers focusing on the evolution of the project using a base model. DDD aids you to effectively model the real world in your application and use OOP to encapsulate the business logic of your organization.

In this article, you can learn how to use PHP to manage your company's projects using DDD and effectively model real-world situations to help implementing your application business logic in a way that is very easy to understand.




Contents

Introduction

What is a Domain Model?

Ubiquitous Language

Multilayered Architecture

Object-values and Entities

Aggregates

Factories

Repositories

Implementation in Laravel

Conclusion


What is a Domain Model?

A Domain Model is the understanding of the context of the problems that an application attempts to solve. Let me explain this in more detail here. The "context" means the type of market in the business world that you work with and the problems that the application is designed to solve.

For example, if you want to develop an application for online food delivery, the context is everything (tasks, business rules, etc.) related with the food delivery processes that need to be implemented in your project.

The domain model is a structured definition of the solution for the problem. It should be like a dictionary with key concepts about the tasks of the problem context.

Ubiquitous Language

«Ubiquitous Language» - this is the language used by business professionals to describe the domain model. This means that the development team consistently uses this language in all interactions and in code. The language should be based on the region model. Let me give you 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 segment above, we create a new product but in the actual application the product details must also be added first to the system:

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

If someone creates a product in a development environment and then adds the product to the system without consistency checks, the application will break. 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 also be changed. We would have two different definitions for one term.

Multilayered architecture

In this article, I'm not going to talk about object-oriented design. But using DDD implies that you apply basics of good software design. Eric Evans (author of "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 - combining the model and implementation. Multilayered architecture is one of its blocks.

Multilayered architecture is an approach that consists in isolating each part based on many years of experience and collaboration of developers. The layers are listed below:

  • User interface
  • Application Layer
  • Domain Layer
  • Infrastructure Layer

The user interface is responsible for displaying information to the user and processing his commands of interaction. In Laravel, the display is a layer of the 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, controllers are located right here.

The domain layer is the heart of business software. This layer is responsible for the presentation of business concepts, information about the business situation and business rules.

The infrastructure layer provides general technical capabilities that support higher layers, and also supports a structure of interactions between the four layers (which is why the repositories are in this layer).

Communication 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 bottom layers are meant to be connected to the top layer, they should use patterns such as Callback or Observer.

Objects-values and entities

I am a big fan of Value Objects. I think they are the essence of the OOP. Although in DDD value objects seem to be simple, they are a serious source of confusion for many, including myself. I have read and heard so many different ways of describing value objects from different points of view. Fortunately, each of the different explanations, helped me to go in depth and understand better object-values, rather than contradict each other.

Value objects are accessible by their meaning, and not by the identifier. These are immutable objects. Their values rarely change and do not have a life cycle. This means they are not like rows of database tables that can be deleted, like instance from tables of 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 an object-value with its own set of behaviors.

The code below shows an example of an object-value 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 a number of database tables. The entity is changeable because it can modify its attributes (usually using setter and getter functions), and they also hava a life cycle, I mean entities can be deleted.

An 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 an object-value.

Aggregates

A model can contain a large number of domain objects. Regardless of how much we envisage in the modeling of the region, it often happens that many objects depend on each other, creating a set of relationships, and you can not be sure of the result by 100%. In other words, you need to know about the business rule that must always be respected in your domain model. Only with this knowledge can you confidently talk about your code.

Aggregates help to 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 by establishing rules for relations between entities. Note that aggregates can also have properties, methods, and invariants that do not fit into one class.

Eric Evans in his book has established some rules for the implementation of aggregates, and I list them below:

  • Root object has a global identity and is ultimately responsible for verifying the invariants.
  • Root objects have a global identity. Internal entities have a local identity, unique only within the aggregate.
  • Nothing outside the boundary of the aggregate can contain a reference to anything inside, except for the root object. A root object can bind links to internal objects to other objects, but these objects can only use them temporarily and cannot be bound to a link.
  • As a consequence of the above rule, only the base roots can be obtained directly from the database queries. All other objects should be found by traversing the associations.
  • Objects inside the aggregate can contain references to other aggregate roots.
  • The delete operation should delete everything at once within the common boundary. That is easy with garbage collection because there are no external references to anything except the root. Remove the root and everything else will be collected.
  • When a change is made to an object at the boundary of the aggregate, all the invariants of the aggregate must be satisfied.

Factories

In the OOP world, a Factory is an object that is responsible only for creating other objects. In DDD, factories are used to encapsulate the knowledge required to create objects. Factories are especially useful for creating aggregates.

The root of the aggregate, which provides the factory method for creating instances of another type of aggregate (or internal parts) will have the 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 which protects the client from dependence on a particular class.

There are times when a factory is not needed and a simple design is enough. Use the constructor when:

  • The design is not complicated.
  • The creation of an object is not associated with the creation of others, and all the necessary attributes are passed through the constructor.
  • The developer is interested in the implementation and, perhaps, wants to choose a strategy for use.
  • Class is a type. There is no hierarchy, so there is no need to choose between a list of specific implementations.

Repositories

A repository is a layer that is located between the domain of your project and the database. Martin Fowler in his book "Enterprise Application Templates" writes that the storage is an intermediate interaction between the domain and the data mapping layer using an interface similar to the collection for accessing domain objects. This means that you need to think about accessing data in your database just as you would with standard collection objects.

Let me explain a little more. Imagine that in DDD you may need to create an object either using the constructor or using the 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 is a problem because you need to make sure that developers always have a reference to the required object. This will lead to the creation of a number of developer associations that are not really needed.

Another reason why storage containers are of great importance is access to the database. The programmer does not need to know the details necessary to access it. Since the database is at the infrastructure level, it has to deal with a lot of infrastructure details, not with domain concepts. In addition, if the developer requests the launch of a request, this will lead to the disclosure of even more internal parts required by the request.

If we do not have a repository, 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 the aggregates will be useless.

Finally, the repository acts as a storage location for objects available around the world. The repository can also include a strategy. It can access one permanent repository or another based on the specified strategy.

Implementation in Laravel

As you might already 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 on 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 can contain many comments, and each comment can contain some sub-comments. Administrators can approve or reject comments after they are added.

In the above scenario, the first step is to create a basic repository in the domain level, a basic repository derived from the Doctrine EntityRepository, which will allow us to have all the built-in functions of the Doctrine repository. Here, we can also use our common functionality, and all our repositories must be inherited 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 store, and the second is the comment store. All repositories must have the entityClass property to define the entity class. In this case, we can encapsulate (private property) the 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 code above, I define relationships in object annotations. Implementing relationships in Doctrine ORM can seem very complicated, but in fact it's not so difficult when you get to know how everything works.

The only way to add comments is to call addComments on the page object, and this method takes only the entity of the comment 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 PageId is valid; I mean that you can not access comments without PageId. For example, comments without a valid page id have no meaning and are inaccessible. In addition, there is a method of factory comment, which 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 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)
            {
                // 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 Authorization( \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 the 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, which is responsible for validating the input data. This type of verification has a definition in DDD, and is also added to laravel 5+.

Conclusion

To cover all of these definitions, many articles will be required, but I did my best to generalize them. It was just a simple example of the root of the unit, but you can create your own complex aggregate, and I hope that this example will help you.




You need to be a registered user or login to post a comment

1,499,804 PHP developers registered to the PHP Classes site.
Be One of Us!

Login Immediately with your account on:

FacebookGmail
HotmailStackOverflow
GitHubYahoo


Comments:

No comments were submitted yet.



  Blog PHP Classes blog   RSS 1.0 feed RSS 2.0 feed   Blog PHP Domain Driven Des...   Post a comment Post a comment   See comments See comments (0)   Trackbacks (0)