4

Background

This is a long and complicated question. I'm using php/MySQL as an example (since it's an actual example) but this could theoretically apply to other languages. Bear with me.

MVC Framework with no ORM

I'm creating an application that uses its own framework. For a variety of reasons, the following answers to this question will not be accepted:

  • Why don't you just use X framework?
  • Why don't you just use X ORM?

This application does its own queries (written by me) and that's how I'd like it to stay for now.

Business Logic?

"Business Logic" seems to be a meaningless buzzword, but I take it to essentially mean

The queries and the logic that builds the result set based on those queries

I've also read that the Model in an MVC should do all the business logic.

User.php is 884 lines

Now that I've gotten my app working fairly well, I'd like to refactor it so as not to have such abominations. User.php is essentially the model for Users (obviously). There are some responsibilities I see in it that I could easily pluck out, but a major hurdle I'm running into is:

How can I reconcile SOLID with MVC?

The reason that User.php has grown so large is because I do any query that requires a User member in that file. User is used for a ton of operations (it needs to do so much more than just CRUD), so any query that needs userid, username, etc. is run by a function in this file. Apparently the queries should be in the model (and not the controller), but I feel that this should definitely be split up somehow. I need to accomplish the following:

  • Create an API that covers all of these necessary queries in a compartmentalized way
  • Avoid giving access to the DB connection class when it's not necessary
  • Add User data to the view (User.php is doing that right now -- the view object is injected by a setter, which I think is also bad).

...so what I could do is create other objects like UserBranchManager, UserSiteManager, UserTagManager, etc. and each of those could have the relevant queries and the DB object injected to run those queries, but then how do they get the coveted User::$userid that they need to run these queries? Not only that, but how could I pass Branch::$branchid? Shouldn't those members be private? Adding a getter for them also makes that pointless.

I'm also not sure where to draw the line of how much an object should do. A lot of the operations similar but still different. A class for each one would be huge overkill.

Possible Answer

If I can't get any help, what I'll do (or at least try to do) is have a dependency injection container of some kind build dependencies for the objects above (e.g. UserBranchManager) and inject them into the relevant controller. These would have a DB and Query object. The Query object could be passed to low level models (like User) to bind parameters as needed, and the higher level models or whatever they are called would give the results back to the controller which would add the data to the template as needed as well. Some possible hurdles I see are creating proper contracts (e.g. the UserController should preferably depend on some abstraction of the user models) but some specifics are inevitably required, especially when it comes to the view.

Can anyone offer some wisdom in response to my rambling question?

Response to @tereško

He has provided a great answer not only here, but also at How should a model be structured in MVC?

Code

As requested, here is some extremely pared down code (basically services one request). Some important notes:

  • Right now, controllers are not classes, just files
    • The controller also handles a lot of the routing
  • There are no "view" objects, just the templates
  • This will probably look really bad

These are also things to improve, but I'm mostly worried about the model (User in particular since it's getting out of control):

#usr.php -- controller
$route = route();
$user = '';
$branch = '<TRUNK>';
if (count($route) > 0) {
   if (count($route) > 1) {
      list($user, $branch) = $route;
   }
   else {
      list($user) = $route;
   }
}

$dec = new Decorator('user');
$dec->css('user');

if (isset($_SESSION['user']) && $_SESSION['user']->is($user)) {
   $usr = $_SESSION['user'];
}
else {
   $usr = new User(new DB, $user);
}
$usr->setUpTemplate($dec, $branch);
return $dec->execute();

# User.php -- model
class User {
   private $userid;
   private $username;
   private $db;

   public function __construct(DB $db, $username = null) {
      $this->username = $username;
      $this->db = $DB;
   }

   public function is($user) {
      return strtolower($this->username) === strtolower($user);
   }

   public function setUpTemplate(Decorator $dec, $branch) {
      $dec->_title = "$this->username $branch";
      // This function runs a moderately complicated query
      // joining the branch name and this user id/name
      $dec->branch = $this->getBranchDisplay($branch);
   }
}

Questions about answers

Answer here:

  • You talk about leaving off caching/authentication/authorization. Are these important? Why aren't they covered? How do they relate to the model/controller/router?
  • The Data Mapper example has the Person class with methods like getExemption, isFlaggedForAudit .. what are those? It seems like those calculations would require DB data, so how does it get them? Person Mapper leaves off select. Isn't that important?
  • What is "domain logic?"

Answer 5863870 (specifically the code example):

  • Shouldn't these factory objects be abstractions (and not rely on creation via new) or are these special?
  • How would your API include the necessary definition files?
  • I've read a lot about how it's best for dependencies to be injected in the constructor (if they're mandatory). I assume you set the factories in this way, but why not the objects/mappers/services themselves? What about abstractions?
  • Are you worried about code duplication (e.g. most models requiring an _object_factory member in their class definition)? If so, how could you avoid this?
  • You're using protected. Why?

If you can provide any specific code examples that would be best since it's easier for me to pick stuff up that way.

I understand the theory of what your answers are saying and it's helped a lot. What I'm still interested in (and not totally sure of) is making sure that dependencies of objects in this API are handled in the best way (the worst would be new everywhere).

Community
  • 1
  • 1
Explosion Pills
  • 176,581
  • 46
  • 285
  • 363
  • 2
    I favourited this question: http://stackoverflow.com/questions/5863870/how-should-a-model-be-structured-in-mvc – Sherlock Jul 19 '12 at 13:23

2 Answers2

2

Dont confuse SOLID (You can get a good explanation of what it is on my blog at: http://crazycoders.net/2012/03/confoo-2012-make-your-project-solid/

SOLID is great when considering the framework that goes around the application you are trying to build. The management of the data itself is another thing. You can't really apply the Single Responsibility of the S of SOLID to a business model that RELIES on other business models such as User, Groups and Permissions.

Thus, you have to let some of the SOLID go when you build an application. What SOLID is good for, is to make sure your framework behind your application is strong.

For example, if you build your own framework and business model, you will probably have a base class MODEL another for DATABASEACCESS, just remember that your MODEL shouldn't be aware of how to get the data, just know that it must get data.

For example:

Good: 
    - MyApp_Models_User extends MyApp_Framework_Model
    - MyApp_Models_Group extends MyApp_Framework_Model
    - MyApp_Models_Permission extends MyApp_Framework_Model
    - MyApp_Framework_Model
    - MyApp_Framework_Provider
    - MyApp_Framework_MysqliProvider extends MyApp_Framework_Provider

      In this good part, you create a model like this:
      $user = new MyApp_Models_User(new MyApp_Framework_MysqliProvider(...));
      $user->load(1234);

This way, you will prevent a fail in the single responsibility, your provider is used to load the data from one of the many providers that exist and your model represents the data that you extracted, it doesn't know how to read or write the data, thats the providers job...

Bad way:
    - MyApp_Model_User
    - MyApp_Model_Group
    - MyApp_Model_Permission

      define('MyDB',   'localhost');
      define('MyUser', 'user');
      define('MyPass', 'pass');
      $user = new MyApp_Models_User(1234);

Using this bad method you first of all break the single responsibility, your model represents something and also manages the input/ouput of the data. Also, you create a dependency by stating that you need to define constants for the model to connect to the database and you completly abstract the database methods, if you need to change them and have 37 models, you'll have TONS of work to do...

Now, you can, if you want work the bad way... i still do it, i'm aware of it, but sometimes, when you have crappy structure and want to refactor, you can and should work against a principle just to refactor correctly and slowly, THEN, refactor a little more and end up with something SOLID and MVC looking.

Just remember that SOLID doesn't apply to everything, it's just a guideline, but it's very very good guideline.

Mathieu Dumoulin
  • 11,584
  • 6
  • 38
  • 65
1

Well .. it depends on what is actually inside your ./user.php file. If i had to guess, you would be a situation, where your user "model" has too many responsibilities. Basically, you are violating single responsibility principle and not sure how to go about fixing that.

You did no provide any code .. so , lets continue with guessing ...

It is possible that your user "model" is implementing active record pattern. This would be the main source of SRP problems. You could watch this part of lecture slides. It will explain some of it. The point would be, instead of using active record, to go with something similar to a data mapper pattern.

Also, you might notice that some of the domain logic, which works with User class instances, seems to happen outside your "model". It might be beneficial to separate that part in a different structure. Otherwise you will be forcing the domain logic inside the controller. Maybe this comment could shine some light on the whole subject.

Another thing you might have crammed inside your user "model" could be parts of the authorization (no to confuse with authentication) mechanism. It could be pragmatic to separate this responsibility.

Update

  • You talk about leaving off caching/authentication/authorization. Are these important? Why aren't they covered? How do they relate to the model/controller/router?

    Caching is something that you would add later in the application. The domain objects do not care where the data comes from. For that reason you can wither add the caching with in the service-level objects or inside the existing data mappers. I would advise to choose former option, because changing existing mappers might have unforeseen side effects. And because it would just over-complicate the existing mappers.

    namespace Application\Service;
    
    class Community{
    
        public function getUserDetails( $uid )
        {
            $user = $this->domainObjectFactory->build('User');
            $cache = $this->cacheFactory->build('User');
    
            $user->setId( $uid );
    
            try
            {
                $cache->pull( $user );
            }
            cache( NotFoundException $e)
            { 
                $mapper = $this->mapperFactory->build('User');
                $mapper->pull( $user );
                $cache->push( $user );
            }
    
            return $user->getDetails();
    
        }
    }
    

    This would illustrate a very simplified acquisition of user information based on user's ID. The code creates domain object and provides it with ID, then this $user ovject is used as condition to search for cached details or, if it fails, fetching that pulling that information from DB via the data mapper. Also, if that is successful, the details are pushed into the cache, for next time.

    You might notice, that this example did not handle situation, when mapper cannot find such user with such ID in storage (usually - SQL database). As I said , it's a simplified example.

    Also, you might notice, that this sort of caching can be easily added on case-by-case basis and would not drastically change how your logic behaves.

    Authorization is another part, which should not directly influence your business logic. I already linked my preferred way for providing authentication. The idea is that, instead of checking for credentials inside controller (like here, here, here or here), the access rights are checked before you execute a method on the controller. This way you have additional options for handling the denial of access, without being stuck within a specific controller.

  • The Data Mapper example has the Person class with methods like getExemption(), isFlaggedForAudit() .. what are those? It seems like those calculations would require DB data, so how does it get them? Person Mapper leaves off select. Isn't that important?

    The Person class is a domain object. It would contain the part of domain logic, that is associated directly with that entity.

    For those methods to be executed, the mapper should at first load the data. In PHP it would look something like this:

    $person = new Person;
    $mapper = new PersonMapper( $databaseConnection );
    
    $person->setId( $id );
    $mapper->fetch( $person );
    
    if ( $person->isFlaggedForAudit() )
    {
        return $person->getTaxableEearnings();
    }
    

    The names of methods in the PersonMapper are there as an example, so that you would understand, how the class is supposed to be used. I usually name methods fetch(), store() and remove() (or push/pull/remove ... depends on how much GIT have I been using). IMHO, there is no point to have a separate update() and insert() methods. If object's data was initially retrieved by mapper, then it is an UPDATE. If not - it is an INSERT. You can also determine it whether the value, which corresponds to PRIMARY KEY has been set or not (in some cases, at least).

  • What is "domain logic?"

    It is part of the code which knows how to create an invoice and apply discount price for specific products. It's also the code which makes sure, that you do not submit registration form, you do not state that you have been born in 1592.

    The MVC is made from two layers: presentation (can contain: views, templates, controllers) and model layer (can contain: services, domain objects, mappers). The presentation layer deals with user interaction and responses. The model layer deals with business and validation rules. You could say that domain business logic is everything in model, that does not deal with storage.

    I guess there is no easy way to explain.

  • Shouldn't these factory objects be abstractions (and not rely on creation via new) or are these special?

    Which "factory objects", where? Are you talking about this fragment ?

    $serviceFactory = new ServiceFactory(
        new DataMapperFactory( $dbhProvider ),
        new DomainObjectFactory
        );
    $serviceFactory->setDefaultNamespace('Application\\Service');
    

    That whole code fragment is supposed to be in bootstrap.php file (or you might be using index.php or init.php for that). It's the entry point for the application. It is not part of any class, therefore you cannot have *tight coupling" there. There is nothing to "couple with".

  • How would your API include the necessary definition files?

    That fragment is not entire bootstrap.php file. Above that code are includes and initialization for autoloader. I am currently using dynamic loader, which lets classes from same namespace to be located in different folders.

    Also, such autoloader is a development-stage artifact. In production code you have to use loader, which works with predefined hashtable and does not need to actually walk the tree of namespaces and locations.

  • I've read a lot about how it's best for dependencies to be injected in the constructor (if they're mandatory). I assume you set the factories in this way, but why not the objects/mappers/services themselves? What about abstractions?

    What are you talking about ?!?

  • Are you worried about code duplication (e.g. most models requiring an _object_factory member in their class definition)? If so, how could you avoid this?

    Did you actually LOOK at how old that code fragment in comments was ?!?!?

  • You're using protected. Why?

    Because, if values/methods are defined with protected visibility, you can access them when you extend the original class. Also, that code example was made for old version of answer. Check the dates.

And no. I will not provide any specific code examples. Because each situation is different. If you want to do copy-paste development, then use CakePHP or CodeIgniter.

Community
  • 1
  • 1
tereško
  • 56,151
  • 24
  • 92
  • 147
  • Thanks for your help. I've made a big update to my question to include some code and ask for further clarification about some things. Some of them may be out of the scope of this question, but I would appreciate the help. – Explosion Pills Jul 19 '12 at 16:03
  • @ExplosionPills , answered your follow-up questions. – tereško Aug 11 '12 at 03:46
  • thank you for answering my questions. I will answer your own follow up questions later on (a bit busy right now). As for code examples, I was not intending to copy anything you had written, I just want to see the way you think. Describing it with words is okay, but only the code can really speak. Now, I'll have to implement your suggestions blindly. It may or may not come out how you intended, so it's hard to tell without a reference. – Explosion Pills Aug 12 '12 at 16:18