cakephp / cakephp

CakePHP: The Rapid Development Framework for PHP - Official Repository
http://cakephp.org
MIT License
8.68k stars 3.42k forks source link

RFC: Use Model folder for Business layer #11260

Closed dereuromark closed 5 years ago

dereuromark commented 7 years ago

This is a (multiple allowed):

Refs https://github.com/cakephp/cakephp/issues/10202#issuecomment-299416583

Yes, I agree there could be some general pattern for Business layer. If we suggested the "CakePHP way" of implementing business logic, this could help developers write better code.

Status Quo

Too much non-ORM business logic munched together with the queries and DB interaction inside of Table classes. Makes sense as no one knows how to use some factory pattern to use a more generic Business layer before directly interacting with our "persistence" layer in Model/Table.

Suggestions derived from the discussion

Let's directly use the /src/Model folder

// base class
src/Model/Model.php

// concrete business class
src/Model/CalculationModel.php

// load via factory (or also via trait)
$calculationModel = ModelRegistry::get('Calculation')

The model could have the Table loading trait then to make $this->loadModel() Table calls internally. Even though I don't like the word model here too much in that method. Also problematic: Nesting then, as the sub-namespaces Table, Entity, Behavior etc are collisions.

We could also use a completely free namespace, of course.

Any other BC ideas that would be minimal invasive but allows to do separation as opt-in? We could get this started in 3.6 even then.

inoas commented 7 years ago

👍 this is a very important part that "didn't make the cut" when transitioning from 2.x "active-record-all-logic-in-one-place" to 3.x. Especially newbies are left without a "frame" (as in framework) where to place their services, business logic, background processes etc.

Edit:

  1. Should have it's own namespace such as "Logic" aka \App\Model\Logic
  2. loadModel should be required to take a partial or FQNS such as 'Table\OrdersTable' or '\YourPlugin\Model\Table\OrdersTable? ... if we want loadModel() support at all...
robertpustulka commented 7 years ago

@ionas loadModel takes a second parameter as a model type/family. This defaults to Table model.

$this->loadModel('Articles') is equivalent to $this->loadModel('Articles', 'Table')

All it takes to support logic classes in loadModel is to add another factory.

$this->modelFactory('Logic', $logicRegistry);
$this->loadModel('Calculation', 'Logic');

$this->Calculation->doStuff($data);
inoas commented 7 years ago

That's good. However there is still possible clashing with loadModel(), isn't there?

Say an OrdersTable object is bound to $this->Orders.

$this->modelFactory('Logic', $logicRegistry);
$this->loadModel('Orders', 'Logic');

Could we have newModel() (or similar) which works like loadModel() but returns the instance instead of binding it magically into $this->*?

robertpustulka commented 7 years ago

Yes, that's also the case with any other models that share the same registry name.

New method suggestion sounds good. Although I think newModel() suggests that you get a new instance with every call. How about getModel()?

inoas commented 7 years ago

getModel() doesn't sound like it creates something if it ain't there, fetchModel() createModel(),initModel()?

robertpustulka commented 7 years ago

get*() in CakePHP usually creates something if it does not exist. See TableLocator/TableRegistry, Validators, EventManager etc.

markstory commented 7 years ago

Instead of overloading model further, should we add skeleton hooks for 'services'. That seems to be close to the missing layer you speak of.

inoas commented 7 years ago

Would it make sense to differ between services that persist things, services that store state and services that are stateless?

ravage84 commented 7 years ago

I like the general idea. 👍

@markstory I get your point about not overloading the Model folder any further. But on the other hand according to the our Cookbook, the "Model layer" of a CakePHP application is for the business logic. It would only be natural to place "domain models" in the Model folder.

As for the name "service" and thus a folder "Service": I know this name is somewhat accepted, e.g. in the Hexagonal Architecture, but I'd argue against it as it is too generic and can be mixed with other web application related meanings, such as "web service".

Personally I prefer the word "domain model" or simply "domain". Also because the business logic is often called "domain logic" interchangeably.

Alternatively, as we are talking about the "business" layer, we could use that word, but I don't like it that much.

In any case, such a layer should be optional as it is not necessary every time. Simply due to unnecessary complexity or because we would not want to overload framework newbies with that burden.

As for when to use that additional layer: I would say, if your controller simply needs data from a data source (whatever kind of), he should use a table object or whatever to fetch that data. But if the controller needs to get some data in a processed/validated/enriched form, it should use a domain model for that. Same goes for saving data...

@ionas I don't think that's a valuable distinction. It would be more relevant to distinguish them between their non technical responsibility and/or domain.

Another thing I have to question is whether such domain model classes should have any framework related base class. I'm not a fan of "abstract away your framework" ™️, but if we add such a domain model layer - or simply a recommended way of doing it - I'm wondering if those shouldn't be plain old PHP classes. Generally, they should not have too many dependencies, otherwise it would be an indicator having to many responsibilties and thus of bad design. A typical dependency could be Table or Entity objects, as they normally would work with/on them.

robertpustulka commented 7 years ago

If the "business layer" was optional we could just recommend a command bus library and provide some conventions and a core plugin for CakePHP integration as @markstory once suggested.

There's a simple plugin for Tactician that we can base on and include in the core eventually: https://github.com/robotusers/cakephp-tactician

markstory commented 7 years ago

if we add such a domain model layer - or simply a recommended way of doing it - I'm wondering if those shouldn't be plain old PHP classes.

This is one reason we don't have domain services/domain models in CakePHP today. If the models/services are basic PHP classes there isn't much that a framework can really add to that beyond the traits we offer today.

dereuromark commented 7 years ago

If the models/services are basic PHP classes there isn't much that a framework can really add to that beyond the traits we offer today.

That's why I think this has to be part of the core philosophy - and should be used and provided by it. Otherwise that will never happen really, and a lot of people will continue to do what is written above. We need to find a way to have the same rapid development strategy, some factory pattern and some good conventions here coming from the core directly for people to learn from, adapt and write their own code in a similar fashion then.

Similar to loadModel() I am fine with sth like loadService() etc and loading classes from Service namespace, we just need to make sure collisions are low, maybe be using a suffix here?

$this->loadService('MyCool')

$this->MyCoolService->foo();
//or similar to behavior collection
$this->services()->MyCool->foo();

The latter can also be easily IDE supportable via IdeHelper Plugin.

makallio85 commented 7 years ago

This is indeed something that is more than welcome.

ravage84 commented 7 years ago

If the models/services are basic PHP classes there isn't much that a framework can really add to that beyond the traits we offer today.

Sure, a framework can do less in such a case. Thouch the following come to mind:

We need to find a way to have the same rapid development strategy, some factory pattern and some good conventions here coming from the core directly for people to learn from, adapt and write their own code in a similar fashion then.

While I agree to some extend, for me proper dependency injection would be crucial (and I'm not talking about a DI container :trollface: ).

How does the following look for you?

// src/Controller/RecipesController.php

class RecipesController extends AppController
{

    public function index()
    {
        // A controller action working with a table directly,
        // as it only fetches raw data to display
        $recipes = $this->Recipes->find('all');
        $this->set(compact('recipes'));
    }

    public function choco_cake($ingredients)
    {
        // A controller action working with a domain model,
        // as it contains business logic
        $recipeModel = $this->loadModel('Recipe');
        $chocoCakeRecipe = new \App\Model\Domain\Recipe\ChocoCake($recipeModel);
        $chocoCake = $chocoCakeRecipe->bakeWith($ingredients);
        $this->set(compact('chocoCake'));
    }
}
markstory commented 7 years ago

@dereuromark If we did have loadService() how would service dependencies be registered? and later on resolved/injected? That is something a DI container does quite well.

dereuromark commented 7 years ago

Yes, a DI container sounds more than useful here, I agree.

@ravage84 I dont like this as it bears no value to inject a factorized low level model via constr. dep. Better to factorize the new service model directly, which internally can call loadModel() if needed.

burzum commented 7 years ago

Too much non-ORM business logic munched together with the queries and DB interaction inside of Table classes.

Well, that's the fault of the developer or a lack of knowledge? We've been using additional classes since CakePHP 2.x and haven't stuffed everything into the a model or table class.

Makes sense as no one knows how to use some factory pattern to use a more generic Business layer before directly interacting with our "persistence" layer in Model/Table.

I guess it's not written in stone that you must use a factory here or a DI-container. If you look at the past 10-15 years there is an ongoing trend talking about the same thing over and over just calling it "service", "manager", "domain object", "model object" or something else and at the end of the day it becomes a very philosophical debate on how to call it instead of focusing on the principle: To separate the business logic into it's own place, somewhere between the controller / shell and whatever else is needed like the "persistence layer".

So I agree mostly with @ravage84:

dereuromark commented 7 years ago

Suggest a structure and tools / classes that could be used but leave the implementation up to the developer

I am fairly certain that also the core has room for improvement here on multiple classes. So no, I don't think this should be a hidden docs addition only :)

davidyell commented 7 years ago

Nice idea, I've been using the src/Lib for this mostly in my projects.

dereuromark commented 7 years ago

Me too, but then you usually "new MyLib()" everywhere and if you do that also in plugins you completely lose the extendability and testing factor.

inoas commented 7 years ago

Lib sounds really like vendor to me.

burzum commented 7 years ago

There aren't so many options then?

I really dislike service. It's very generic and by this as well hard to search. Try to search for "some-framework service" and you'll probably find all kind of crap results, just no practical guide on how to implement the business layer.

inoas commented 7 years ago

Leaves us with just Domain, right? We could also have App\Domain\Model, App\Domain\Logic App\Domain\Persistence?

burzum commented 7 years ago

Domain alone is also pretty generic and especially in a web framework where you always deal with domain(s). My personal preference is BusinessLogic, because it's broad enough to be anything within the scope of your "business", your "application" and it's not easy to mistake for something else like "service" or "domain" in the context of a web app. "Logic" alone applies to everything. A controller contains logic, models contain logic... 😄 And "Persistence" is completely wrong IMHO.

inoas commented 7 years ago

IMHO:

markstory commented 7 years ago

I've used Service in my applications as that is the conventional name outside of CakePHP as well. Other PHP frameworks and literature like Domain Driven Design use this term to reference the thing we're discussing.

bancer commented 6 years ago

My 5 cents: If I understand correctly table classes use the table data gateway pattern according to M.Fowler, entity classes - the row data gateway. So it would be logical to have domain classes that use the domain model.

T0mats0 commented 6 years ago

Where i work, using cake, we have solved this problem using di and auto loading. Different areas of concern have their own directories that are autoloaded. There isn't a single naming convention. Di is super useful. Otherwise we would have way to many dependencies to new-up all over the place. Even though the di is sometimes used as a service locator it is way better than the alternative.

odan commented 6 years ago

The term Domain could be confused with Domain Driven Design (DDD). People would ask: Where is the Infrastructure folder / layer? The term Service fits well and is already established as a layer for business logic.

Where to put the business logic in MVC?

The user interfaces with the view, which passes information to a controller. The controller then passes that information to a model (layer), and the model passes information back to the controller. The controller effectively stands between the view and the model.

In MVC, the business logic (Service layer) is part of the model layer (The M) and can be called directly from the controller. The service layer validates the data in the respective context, calculates the data using table classes and returns the result.

Put the business logic in a Service. The reason is that you end up respecting principles like Single Responsibility an Separation of Concerns. Your Service layer would handle Business Logic and your "Table" would handle Database Operations.

Message flow:

Request -> Controller (action) -> Service > Tables(s) ->
(back to) -> Service -> Controller -> View -> Response

I agree, but even in Service, this still means dozens of methods in one class.

No, because the Service is a layer and not a single class. Within the service layer you can create many small and specialized classes for validation, mapping, calculation etc...

dereuromark commented 6 years ago

So I guess we all agree that "Service" would be the right layer. Then we only need to find a good factory or DIC pattern here that allows to create and use those classes cleanly. I do not want to see more new MyBusinessClass() all over the code, as this makes testing and extend-ability (in plugin case) rather impossible.

burzum commented 6 years ago

I would prefer some kind of DIC these days in CakePHP. I think we use the event manager in some cases to work around cases were a DIC might be better. If you want an example check the recent changes to the authorization plugin.

If we want yet another locator, I can contribute this and also a trait that allows you to declare services as annotation. We have an experimental branch with CQRS and services in our app where I started playing around with services, so it would be just a copy and paste job. Also we've used App\Service as namespace. I agree with @odan but I was simply to lazy to put it one level deeper. ;)

bancer commented 6 years ago

The term Service fits well and is already established as a layer for business logic.

I think it is highly arguable. The term "service" is overloaded and means different things in different architectures.

Service is not a canonical or generic software term. In fact, the suffix Service on a class name is a lot like the much-maligned Manager: It tells you almost nothing about what the object actually does. https://softwareengineering.stackexchange.com/questions/218011/how-accurate-is-business-logic-should-be-in-a-service-not-in-a-model/218394#218394

According to M. Fowler:

The key point here is that the Service Layer is thin - all the key logic lies in the domain layer. https://www.martinfowler.com/bliki/AnemicDomainModel.html

dereuromark commented 6 years ago

Then we go with Domain or whatever. In the end the name and need to argue over it is less important than that something actually happens and people can use it :)

burzum commented 6 years ago

@bancer well your own quote of Martin Fowler implies that a service and a domain layer are two different things or have at least two different purposes in the architecture. So domain would be "wrong" as well. 😃 In fact we would have to have \App\Model\Domain and \App\Service? I've intentionally added the Domain NS here to the Model layer because it's the "Domain Model".

But then again, if you read the linked article there is this:

Domain Layer (or Model Layer): Responsible for representing concepts of the business, information about the business situation, and business rules. State that reflects the business situation is controlled and used here, even though the technical details of storing it are delegated to the infrastructure. This layer is the heart of business software.

Well, we could see the table models as kind of domain model because they implement rules to some extend.

It goes on with:

In general, the more behavior you find in the services, the more likely you are to be robbing yourself of the benefits of a domain model. If all your logic is in services, you've robbed yourself blind.

Honestly, I prefer to see it a little more pragmatic. Until you have a giant app spending all the time thinking about what goes were and having thousands of objects is overkill as long as your code is split in logical pieces. How the hell you call these pieces isn't that important, important is that they are properly abstracted and represent only one "domain" 😆 of logic / data. Basically keep it simple until you need it.

So an ideal world might be: Controller -> Services -> Domain Models -> Table/Repository?

But when the service is supposed to be slim, I'm not sure what exactly is left there. Sounds similar to a controller to me then.

An intermediate step we're about to do at work here is to remove all logic is inside table objects, except custom finders, from them and move them into a - whatever you like to call it - different layer of objects. We already have objects that implement specific part of our business logic and are not part of any existing NS of the framework.

We've hired @rosstuck for consulting to identify what we could do better architecturally in our app and I've asked him if he would provide his opinion on that topic for the framework. We should approach him and ask if he has time and mood to give us his opinion on that topic.

bancer commented 6 years ago

@burzum What I wanted to communicate is that the term "service" is overloaded with many different meanings. I did not mean that there is no need for the business logic layer or that there is need for another extra layer. "Domain" is closer to the meaning of "business logic" than "service" I suppose. In my opinion any of your names proposed above ("App\DomainModel", "App\BusinessLogic", "App\BusinessModel", "App\BusinessLayer") would be better than "App\Service".

robertpustulka commented 6 years ago

On a side note: In my new app I decided to put business logic into App\Model\Logic. There's where I keep all my classes that do complex logic on tables: both persistence and complex queries as well.

odan commented 6 years ago

To be honest, the terms Domain and Service are very confusing and are used differently depending on the context and architectural style. On the other side Service is generic but specific enough in this context. Instead of naming the business logic folders by type, we could try to apply a Module-oriented code structure for the business logic. Read more .

The src/Service directory could act as root directory for all modules / sub-systems. This directory contains an additional subfolder for each sub-module (e.g. src/Service/Customer/, src/Service/Invoice/).

A module is a logical unit that can represent a feature or a process etc. All classes that belongs together can be bundled within a module. When working on a special feature, the developer needs to move mainly in the module folder provided for this purpose.

The only exceptions (for me) are the folders like Controller, View and Template since these are of a "technical nature" (Infrastructure). It would be helpful to move the Table and Entity directory under the src/ directory.

Example:

src/
  Controller/
  Table/
  Entity/
  Service/
    Customer/
      CustomerRepository.php
      CustomerService.php
    Invoice/
      InvoiceRepository.php
      InvoiceService.php
  Template/
  View/
dereuromark commented 6 years ago

would be helpful to move the Table and Entiy directory under the src/ directory.

Then you are forgetting behaviors and other Model subfolders. We should rather go with @robertpustulka and use Model/Logic/ etc. Especially if it will follow similar factory techniques the Table classes do.

Otherwise an own folder Business would make more sense to keep it separated from the (ORM mixed) Model layer.

odan commented 6 years ago

@dereuromark Sounds good. I would put a lot of emphasis on not mixing the business logic too much with the ORM. I would even abstract the ORM with a Repository to simplify the migration to the next framework version. For this reason I would prefer a separate folder. But I think it's just a matter of taste.

inoas commented 6 years ago

I still consider Business part of the Model, but Model/Business, Business, Model/Logic all work for me

rosstuck commented 6 years ago

Hi, not a Cake user but just popping my head in as someone who's also had to tackle some of these as well. :smile:

In general, I'd avoid Service because it's a very very overloaded term these days. Also, there's Domain Services and Service Layer (which aren't the same thing) so even if folks knew about roughly the scope it still wouldn't be quite clear which one is meant.

In the past, I've often split things first by module or section (so, UserManagement, Invoicing, etc) then had separate folders for Domain, Infrastructure, UI, etc. Alternately, you can take more of a Ports & Adapters approach since that might make the naming and number of concepts introduced clearer?

All of this depends on if you're building a RAD CRUD app or a larger, more complex app though. Not every application needs a full blown domain layer or service layer (though probably more need them and don't have them than the other way around). So, perhaps there's two recommended setups?

That said, with a PSR-0/PSR-4 autoloader (and yes, a DI container would be nice too :wink:) then really this should just be up to the user (albeit with some helpful documentation).

If anyone would like to talk further in an easier format, I'm a bit backlogged this month but I have some time in May, so feel free to reach out then if I can help. Otherwise, I'll keep my trap shut! Thanks!

burzum commented 6 years ago

So what about having these folders in 4.0 then?

The terms should be pretty clear and easy to distinguish in this case. Especially DataModel will make clear that the stuff in there is just about the data. Model alone seems to be to generic.

inoas commented 6 years ago

I like Model because it is generic. Schema, Validation, Validator, Entity, Table, Persistence, Transformer, Finder, Behavior, Rule, all then kind of make sense.

However if we strive to split up the old Rails2ish Active Record concept into more than just Table + Entity and setup some good generic defaults for Cake3, I'd say we just create one additional and generic namespace within App/Model, and I'd avoid the nomenclature Service as that is specific and possibly ambiguous at the same time. That namespace then is for everything not Table/Entity related, unless the developer wants to be more specific and then can add Service or Schema or Transformer or whatever.

To quote @dereuromark

We should rather go with @robertpustulka and use Model/Logic/ etc.

-> https://github.com/cakephp/cakephp/issues/11260#issuecomment-333530502

If Logic has to much of a stateless touch for you guys, we can go with Business - however both are part of the Model layer and introducing another namespace/layer is hard to teach in MVC/MVP terms especially to newbies imho.

burzum commented 6 years ago

@inoas funny that you dislike "Service" but propose the highly generic "Model" at the same time.

Model is right now used as a dump all folder for everything which is in fact wrong. The whole ORM / active record thing deals with the concern of data mapping. While the framework should be generic, which is the idea of any framework, it should be precise about what goes where. And model right now is clearly data mapping. But we've created a lot sub folders in this folder as well due to the lack of a better place, the framework doesn't provide anything here. We've started to move the stuff outside, refactor and try to go for a DDD approach.

I would like to see Cake providing some guidance to DDD or similar approaches. Something that will lead people to thinking about why something goes were and for the people who are already familiar with these concepts. "Logic" is a bad name as well, everything is logic in code. The more I think about it the more I like "DomainModel" because it is a distinct term and it doesn't limit you to anything you put inside because it is up to you how you model each domain object.

dereuromark commented 6 years ago

Then lets go with src/DomainModel/ before we get lost in terminology wars again instead of trying to actually solve the technical issues behind it :) For me the actual name matters less than having the solution implemented and usable.

burzum commented 6 years ago

@dereuromark I think it is important to have a discussion about naming things right. Actually my main problem with DDD is in fact about the terminology and getting used to it and the right interpretation of what exactly a "service" and what a "domain object" is. 😆

inoas commented 6 years ago

So what is a DomainModel, what is going to be in there, and what is not.

How well does it fit into MVC/MVP? How easy is it to explain to newbies? Why are there suddenly 2 "ModelFolders"?

I still prefer what @robertpustulka suggested and @dereuromark supported https://github.com/cakephp/cakephp/issues/11260#issuecomment-333530502

dereuromark commented 6 years ago

@ionas Wanna take a stab at a PR here? Maybe having sth concrete to discuss makes things easier.

odan commented 6 years ago

@dereuromark The Domain Model is not enough. Look at the Service Layer around the Domain Model. https://martinfowler.com/eaaCatalog/serviceLayer.html

There are two different implementation variants of the Business Logic.

Domain facade

Operation script

Source: Martin Fowler - Patterns of Enterprise Application Architecture

Personally, I prefer the "Operation Script" approach because it has proven itself in my applications.

I try to separate logic from data like this:

Logic

Data

What I wanted to say: A service layer with business logic (opperation script approach) is very useful to separate the logic from the data. :-)

burzum commented 6 years ago

@ionas read what @odan wrote and do your own Google search on "DDD". That's what I did and still do about DDD and architecture.

@odan I disagree that a domain object, which can be composed out of anything (any classes / objects) is part of the data layer.