📜 ⬆️ ⬇️

Solving the problems of organizing business logic in PHP or how to go your own way

image Hi, Habr! This is not the first time I've been trying to write this article, but for a long time there has been a desire to share experiences and people have been asking.

On Habré, there are many articles about various technologies, languages, api, etc., but often programmers in the heat of development forget why all this, below I will try to describe how not to forget about the most important thing when developing.

The article will be about how we organized work with business logic in PHP, combining different approaches.
')
Here it will be described how to get away from the problems of PHP frameworks associated with smearing the subject logic on the controller layer.

I will not guarantee that the solutions presented are some kind of silver bullet, all of the following is just one of the options for approaching the solution of common problems. There are both pros and cons and this approach copes with its main task.

A little bit about how to work with business logic in popular PHP frameworks



Ordinary situation


If we look at the most popular frameworks, the MVC architectural pattern is taken as the basis for them. There are no tools for organizing business logic as such, but for creating simple crud everything is there.

The standard situation that we see in most cases is an anemic model, for example, when the class UserModel is just a set of attributes and does not contain any logic. Business logic is contained in the controller layer.

Controllers are transformed into something between a Transaction Script and Service Layer in the Domain Model. They validate the input data, obtain model entities from the database, and implement the business logic of these entities.

I am not saying that this is wrong, in some cases this is a self-justifying approach. When it is worth developing without any complex architecture for business logic:

  1. With simple business logic
  2. To create mvp
  3. When prototyping

If you do not have such a situation, then you should think about the architecture and the place of business logic in this architecture.

Complex business logic


If the business logic is complex enough, then there are two standard solutions:

Transaction script


To separate business logic from the framework, it is worth making it into separate entities. Using Transaction Script we can create a large set of scripts that handle specific user requests.

For example, if you need to upload a photo, you can create a photo upload script. It can be programmatically separated into a separate class:

PhotoUploadScript
class PhotoUploadScript
{
public function run ()
{
/ * implementation of photo upload script * /
}
}

I advise you to read more about this approach in the book "Architecture of corporate software applications", it describes all its pros and cons.

Domain Model


When implementing the Domain Model, things get significantly more complicated. It is necessary to think over business logic and select entities with clearly shared responsibility. There are many pitfalls here, this is how to organize the facade for the domain and how to avoid creating an application with a ball of connections between business entities, etc.

In the book “Architecture of corporate software applications” it is proposed to introduce a Service Layer layer which will serve as the business logic interface and will consist of several services grouped by common functionality (for example, UserService, OrderService, etc.).

This approach is described in more detail in the books “Architecture of corporate software applications” and also the whole book “Object-Oriented Design (DDD)” is devoted to it, which I personally highly recommend reading.

our story


How it all began


In 2014, it was decided to start a project and the question arose of the choice of language, technology, libraries, etc.

The choice fell on PHP, but it was decided to abandon the use of existing frameworks. The basis was taken old work and decided to give her a new life in a new guise.

In our organization, we understand the importance of such things as testing, business logic, design patterns, etc., which is why it was decided to write a tool controlled by us and allowing PHP to work with business logic.

Architecture, dictionary


The basis was a multi-layered architecture. If we consider a very general and enlarged scheme, then it is possible to select 3 layers:



Something was taken from the “Big Blue Book” on DDD, something from the book “The Architecture of Corporate Software Applications,” and all of this was reworked, if necessary, to fit our needs.

Interface


Interface is in our case the layer responsible for access to the subject area from the outside world.
We currently have 2 types of interface:

The API is a RESTful api consisting of a View and a Controller.

CLI is an interface for invoking business logic from the command line (for example, cron tasks, workers for queues, etc.).

View

This part of the Interface layer is very simple, since our PHP projects are an API only and do not have a user interface.

Accordingly, there was no need to include work with template engines, we just give the view as json.

If necessary, this functionality can be expanded, add support for template engines, enter different response response formats (for example, xml), but we do not yet have such needs. This simplification allowed us to devote more time to more important parts of the architecture.

Controller

Controllers should not contain any domain logic. The controller simply receives the request from the router and calls the appropriate model interface with the request parameters. He can do some small transformation to communicate with the model, but there is no business logic in it.

Model


In the model layer, a basic set of entities was selected on which practically any subject area can be deployed without breaking its isolation from other parts of the architecture.

It was allocated 3 main generalized elements of the model:

Entity is an entity with a set of characteristics in the form of a list of parameters and with behavior in the form of functions.
Context is a script within which entities interact.
Repository is an abstraction for storing an Entity.

Storage


In the Storage layer, we simply have a set of mappers that know how to save which entity from the model to the base, or load from the base.

Detailed theory



View controller


This part of the platform organizes an API interface for business logic from the outside. It consists of many elements, but for understanding it is possible to describe several:

Router - http router requests to the corresponding controller method (everything is standard).

View is essentially a converter of responses from the model transmitted as ValueObjet (here we, too, do not give the view a lot of knowledge about the business logic) in json. Although in the classic MVC view it receives updates from model directly, we do this through the Controller.

Controller is a layer hiding the interfaces of the model. The controller converts the http request to the input parameters for the model, calls the model script, gets the result of the execution and returns its View for display.

This part does not contain any business logic and does not change during the project unless you need to create new API interfaces. We do not inflate the layer of controllers, leaving them compact and simple.

Model


And now let's talk about the most important and valuable element, encompassing business logic.
Let us consider in more detail the main elements of the model: Entity, Context and Repository.

Entity
Abstract class entity
abstract class Entity { protected $_privateGetList = []; protected $_privateSetList = [ 'ctime', 'utime']; protected $id = 0; protected $ctime; protected $utime; public function getId() { return $this->id; } public function setId( $id) { $this->id = $this->id == 0? $id: $this->id; } public function getCtime() { return $this->ctime; } public function getUtime() { return $this->utime; } public function __call( $name, $arguments) { if( strpos( $name, "get" ) === 0) { $attrName = substr( $name, 3); $attrName = preg_replace_callback( "/(^[AZ])/", create_function( '$matches', 'return strtolower($matches[0]);'), $attrName); $attrName = preg_replace_callback( "/([AZ])/", create_function( '$matches', 'return \'_\'.strtolower($matches[0]);'), $attrName); if( !in_array( $attrName, $this->_privateGetList)) return $this->$attrName; } if( strpos( $name, "set" ) === 0) { $attrName = substr( $name, 3); $attrName = preg_replace_callback( "/(^[AZ])/", create_function( '$matches', 'return strtolower($matches[0]);'), $attrName); $attrName = preg_replace_callback( "/([AZ])/", create_function( '$matches', 'return \'_\'.strtolower($matches[0]);'), $attrName); if( !in_array( $attrName, $this->_privateSetList)) $this->$attrName = $arguments[0]; } } public function get( $name) { if( !in_array( $name, $this->_privateGetList)) return $this->$name; } public function set( $name, $value) { if( !in_array( $name, $this->_privateSetList)) $this->$name = $value; } static public function name() { return get_called_class(); } } 


Entity is entities of the domain, for example, a user, an order, a car, etc. All such entities may have parameters: ID, registration time, price, speed. We provide the work with parameters in the base class Entity, and the behavior is already defined by the developer in the heirs classes.

The parameter list is also needed to save Entity to the database, but the model does not know anything about this saving. At the infrastructural level, there are mappers who know how to save entities of the model to the base.

The identifier of the Entity in our platform is a basic parameter, it allows you to uniquely identify different entities within the same class of objects.

More you can say that our Entity is very similar to Entity, which is described in the book by Eric Evans.

As stated in Evans, the identification of the object in software systems is one of the most important tasks. Also Evans specifies that identification is achieved not only due to the same attributes of objects.

What can be considered characteristics that can identify an object in the software system: attributes, object class, behavior. In the development of our Entity, we just repelled from these characteristics.

Attributes can be different depending on the entity, but we identified the identifier in the base as inherent to almost everyone. We set the object class due to OOP and the inheritance of the development language provided to us. Behavior is defined by methods for each class.

Context

Abstract class context
 abstract class Context { protected $_property_list = null; function __construct( \foci\utils\PropertyList $property_list) { $this->_property_list = $property_list; } abstract public function execute(); static public function name() { return get_called_class(); } } 


Context is the script within which Entity interacts. For example, “User Registration” has a separate context, and the work of this context looks like this:

  1. We start a context and we transfer it parameters for registration. At the input context gets a list of simple parameters (int, string, text, etc.).
  2. Validation of parameters validity. Validation here for the domain, we do not check http requests here.
  3. Create user.
  4. Saving user. This is also part of the domain, the main thing is that it is abstracted from where and how we save this user. Here, for the abstraction of conservation, we use Repositories.
  5. Sending by mail notifications.
  6. Return the results of the execution context. To return the result, there is a special class ContextResult which contains a sign of the success of the execution of the context and either data with the results or a list of errors. (at the level of view-controller, model errors are translated into http errors)

Context is almost pure Transaction Script, but with some exceptions. Fowler gives an example of implementing business logic through Transaction Script or Domain Model. When using the Domain Model, he recommends using the Service Layer in which services are created on the basis of common functionality (for example, UserService, MoneyService, etc.). In our case, Transaction Script can act in the same Service Layer if you do not make model entities anemic.

For example, a set of user-related contexts (UserRegContext, UserGetContext, UserChangePasswordContext, etc.) with a non-anemic user is almost equivalent to a UserService (Service Layer). We have contexts that take on a lot of business logic and they can be considered Transaction Script, but there are contexts that just call some kind of Entity functionality and then the whole business logic is hidden from the context and then they are closer to the Service Layer.

From here, in cases with complex business logic, you can either make the DDD system pure or the system, or you can organize the logic through Transaction Script if the business logic is not so complicated and consists of standard scripts to create, query, update, etc.

Repository

Generic repository class
 class Repository { function add( \foci\model\Entity &$entity) { $result = $this->_mapper->insert( $entity); return $result; } function update( \foci\model\Entity &$entity) { $result = $this->_mapper->update( $entity); return $result; } function delete( \foci\model\Entity &$entity) { $result = $this->_mapper->deleteById( $entity->getId()); return $result; } } 


Repositories are an abstraction of business logic for organizing save / delete / update / search for entities in the database. Specific repositories work with specific entities, for example, the UserRepository works with User.

Data storage


Data storage is done using mappers. There is a generalized Mapper class that contains the basic functionality for working with Entity.

Generic Mapper Class
abstract class mapper
{
protected $ _connection;
protected $ _table;

function __construct (Connection $ connection)
{
$ this -> _ connection = $ connection;
}

abstract protected function _createEntityFromRow ($ row);

abstract public function insert (\ foci \ model \ Entity & $ entity);
abstract public function update (\ foci \ model \ Entity & $ entity);
abstract public function delete (\ foci \ model \ Entity & $ entity);

public function getTableName ()
{
return $ this -> _ table;
}
}

For specific entities, they create their own specific mappers that know how to save this entity into the database, how to load it from the database, how to delete it (although actually deleting something from the database is bad practice, so in most cases we just mark it as deleted) and change.

Thus, if we have a class User in the model, then the UserMapper class is created in accordance with it. The logic of working with the database is rendered in a separate layer and can be easily replaced if necessary.

Model entities don't know anything about mappers, but mappers, on the contrary, everyone knows about their model entities.

Generalized schema


Big picture
image

A very general structure of classes of our platform is painted here, many small less important classes are omitted.

As you can see, only this basic structure contains quite a few elements. In a specific project, to all this, a multitude of classes of contexts, entities, repositories, and mappers that implement specific business tasks will be added.

Config and PropertyList on the diagram are utilitarian entities that are used by all.

A little practice (just a little bit)


The theory is good, but let's consider how to put all this into practice. Suppose we have a task to make an application for personal financial accounting. Approximate glossary of terms (I will not paint definitions, everything is simple here): Money, Budget, User, Product, Service, Calendar

Next, make a couple of user scenarios, take something non-standard, for greater clarity. Purchase of goods - we will assume that the purchase for us is just a fact of a one-time debit. Setting the payment service on the calendar - it will be a regular debit with a description.

Select entities: Money, Budget, User, Product, Service, Calendar.
Select 3 contexts according to user scripts:

The first context is simply the purchase of goods.

BuyOrderContex


  1. We receive User from base on input data (for example on a token)
  2. Get or create a new Product
  3. Say Budget to install a Product purchase for a specified amount of Money

Then everything is more complicated, the second user script is divided into 2 contexts, in the first we set when to write off the money, and in the second we make the write-off itself.

SetSchedulePayForServiceContext


  1. We receive User from base on input data (for example on a token)
  2. Get or create a new Service
  3. Install in the Calendar a debit for the Service on a given date

SchedulePayForServiceContext


  1. Whether we look in the Calendar, is there a charge for the current time?
  2. We load Service for which it is necessary to write off money
  3. We write off money for the service

Already in this small example, some advantages and disadvantages of this approach are visible (for example, duplication of logic in different contexts, this is well written in the book “Architecture of corporate software applications”).

Conclusion


Our practices


Separation of functionality


Breaking the entire application into contexts makes it easy to distribute it to different servers.
For example, we have contexts associated with the registration of users, it is easy to take this whole group and transfer it to another server without disrupting the work of the rest of the application.

Context causes context


A context can invoke a context, and so it is possible to build complex chains. This technique is used very rarely. Such an approach also has many pitfalls in it, so there are plans to ban such use.

Cron task


Context as a unit of execution allows you to call it from anywhere. The logic of running contexts as cron tasks is based on this.

SPA


One of our projects is a website in which the client part is completely written in JavaScript and through RESTfull api interacts with the server part in PHP. We didn’t even plan to do this when we started development, but this opportunity to build SPA applications with our platform as a server turned out to be very convenient.

Testing


All code is covered by tests. We use 2 types of tests:

  1. Unit tests, for example for classes like Config
  2. Acceptnce tests, for requests to our api.

Plans


  1. We very much miss the wider distribution of our development. Everything is written exclusively for internal use and it imposes its disadvantages.
  2. Not enough running in high-load projects. We already have several working commercial projects, but they cannot be called high-loaded.
  3. The logic of working with the transaction so far we have not much thought out. At this point, we currently have a slight violation of the encapsulation model. In the future, it is planned to introduce Unit Of Work for abstraction from database transactions.
  4. Testing mainly covers api, in plans to make context testing. So it will be possible to test the domain in isolation.

What we got in the end


  1. A flexible platform to create an application with a clear separation of business logic from infrastructure.
  2. Fully controlled development and it gives a lot of advantages in solving technical problems of controversial issues.
  3. An architecture based on well-known design patterns that allows new developers to quickly get involved in a project.
  4. Vast experience in application of proven approaches in design, which are often forgotten in new projects and do not seek to use them in existing ones.

findings


In general, we are satisfied with the result of the work done. The written platform solves our problems. It allows you to flexibly implement business logic, which greatly simplifies development. There are many plans for improvement and expansion.

PS: If such an approach to solving a problem with a subject area in PHP frameworks will be of interest to the community, then we may well get ready and prepare open source.

PSS: I immediately anticipate phrases to the account of why you need a bicycle, if you already have everything. I see no reason to argue about this, this is our approach and it worked.

PSS: I also anticipate questions, why do Transaction Script and Domain Model incest, and why not do it and not get a flexible tool for solving business problems.

Source: https://habr.com/ru/post/282253/


All Articles