Commands

From GeeklogWiki
Jump to: navigation, search

In our Discussion about views, you took notice that all the view does is render the page. There is no business logic, not validation, nothing but generating HTML that is displayed to the user on behalf of the view. "Where...", you might asks "...does the work get done". The answer is in Commands.

Commands aren't really represented in the acronym for Model-View-Controller (MVC) but they are an integral part of the MVCnPHP implementation. Commands are where all business logic resides...where the work gets done. Each command is atomic, doing only one thing before returning a forward that will instruct the controller where to go next. Don't worry too much about the controller as we will get to how that works soon enough.

So continuing with our sample from the contact manager plugin, we were working on the Contact Editor. Obviously, the contact editor will require a command that will save the contact. That is done by PHPARCH_SaveContactCommand.php located in the commands/ directory in the sample application. If you open that file up in your IDE, we will cover step-by-step what is going on.

 10    protected function authorizedToRun()
 20    {        
 30        $this->contact = unserialize($_SESSION['contact']);
 40
 50        if (is_object($this->contact)) {
 60            if ($this->contact->getAccountId()) {
 70                if ($this->contact->getAccountId() <> $this->user->getAccountId()) {
 80                    return false;
 90                }
100            } 
110        } else {
120            throw new Exception('No contact object was in session');
130        }
120        return true;
130    }

Similar to PHPARCH_ContactEditorView::authorizedToView(), this method implements a security check to ensure the user requesting the action is authorized to do so. This method gets called through the constructor calling change on the view ancestors. Line 10 is working on an assumption that the contact we want to save in is the session. If you recall PHPARCH_ContactEditorView::processView() we stuffed the contact that was edited in session. Now we are grabbing that value out of session and performing the following checks:

  1. First, ensure we got an object from the session.
  2. If we do have an object, get the account ID, if we have one (keep in mind that new contacts won't have an account ID tied to it.
  3. If we did get an account ID for the contact, ensure the current user is tied to that account.

Now all we have to do is validate and save the object.

 10    if (empty($cid)) {		    
 20        // This is a new contact, go get the data from the submitted form
 30        $objs = $this->requestToObjects(array('Contact'=>$this->contact));
 40		    
 50        // Associate contact to user
 60        $objs['Contact']->setAccountId($this->user->getAccountId());
 70    } else {		
 80        // This is an existing contact, let's get the updates from the form
 90        $objs = $this->requestToObjects(
110        array('Contact'=>$this->contact, 
120              'Address_Home'=>$this->contact->getAddressRelatedByHomeAddressId(), 
130              'Address_Work'=>$this->contact->getAddressRelatedByWorkAddressId()));
140    }

Ok, now we are at a point where you really have to pay attention because there is a bit of magic involved with the code above that will save Geeklog 2 developers a lot of time. The IF check is simply checking to see if the contact we have is new or not. If it is new (i.e. empty) then we call requestToObjects(). This method is *very* powerful. In a single call, it will parse the $_REQUEST object looking for any objects on the submitted form, create them and set the values on it. What you get back is an array of object(s) that have all the form data filled in the appropriate member variables for each object. So how does this work? Well, do you recall the funny looking form field names we had? One was Contact->firstName. What requestToObjects does is:

  1. Checks it's internal object array to see if the Contact object has been created yet. If not, it will create the contact object.
  2. On the contact object, it will call setFirstName() and it will give it the value from the form. So NOTE: If you want to save yourself the time from having to create the object yourself and calling the setter() explicitly then you need to ensure the form field names map cleanly to the className->attributeName.
  3. return the array of created objects.

Now, it is quite possible that a single HTML form would modify two objects of the same class. Our Contact Editor example illustrates. this. If you go back to the ContactEditor.thtml, you will notice that there are two sets of Address fields, Address_Work->fieldName and Address_Home->fieldName. requestToObjects() is smart enough to create two different address objects. Be sure you take the time to realize the impact this will have on your development. I wouldn't recommend continuing without this crucial understanding.

So we covered, the IF part of the code snippet above, what about the ELSE part? Looks pretty similar, doesn't it. The only difference is, in the ELSE we are assuming we have a pre-existing contact object that we were editing. So what we do is we pass it and the related address objects to it so that requestToObjects() will use those objects instead of creating new versions of those objects like it did in the IF. So, to be clear, in the IF requestToObjects will create the model objects itself and in the ELSE it will simply use the ones we gave it.

                // Until requestToObjects can handle parent child references we have to do this
		$this->contact = $objs['Contact'];  
		// NOTE: there is not point in creating address records if we didn't get any data in them.
		// the following will check to see if they are empty and, if not, set the address properly.     
		if (!$objs['Address_Home']->isEmpty()) {
		    $this->contact->setAddressRelatedByHomeAddressId($objs['Address_Home']);
		}
		if (!$objs['Address_Work']->isEmpty()) {
		    $this->contact->setAddressRelatedByWorkAddressId($objs['Address_Work']);
		}

The code above is just an exercise of getting a handle to the domain objects we got back from requestToObjects(), and assigning the address objects to the contact appropriately. From this point, we are ready to do data validataion:

    if (!$this->passesValidation()) {
        return 'errors';
    }

Geeklog 2 developers will spend quite a bit of time validating their model objects. passesValidation() is a method on the command that does all those checks. In the event those checks are invalid, a series of errors will be generated and then inside the above IF we return the name of a forward. We haven't covered forwards yet but they are mechanisms by which commands pass execution control on to the appropriate view or command. Just understand in the above code snippet that 'errors' is the name of a forward that instructs the controller where to go from here.

Now, if we pass the data validation we are ready to save the object:

                $dao = PHPARCH_DAO::singleton();
				
		try {
		  // Try saving.  NOTE: this one command will start a transaction, do an insert on the two
		  // address objects and then insert the contact before commiting to the database.
		  $retVal = $dao->save($this->contact);
		} catch (Exception $e) {
		    unset($_SESSION['contact']);
		    // Throw the exception back for now, this really should be handled here
		    throw $e;
		}

Again, more magic happens in the code above. The first line gets a handle to the DAO object (Geeklog_PropelDAO) and inside the TRY block we tell the DAO layer to save the contact. Be sure to note the following:

  1. We did not have to tell the DAO to save the address objects. That's because the address objects are tied to the contact already. The DAO, with the help of Propel, is smart enough to start a transaction, save the two address objects and then save the contact. We'll cover how this is done in later chapters but for now, just know that the DAO layer is transaction aware and can save parent-child relationships in just one command (sweet, isn't?)!
  2. Note that we did not have to write any SQL. Again, the DAO layer (through Propel) generates the SQL for you and it is smart enough to know if it needs to do an INSERT versus and UPDATE.

Then, to end that code snippet, the CATCH block throws an exception back in the even the saved failed. All we have to do is return the name of the forward that let's the controller know where to go now:

                // Now that they are done with the object, remove it from session.
		unset($_SESSION['contact']);
        		
		// Send a message back to the user letting them know the save worked fine. 
		$_SESSION['MVC_MESSAGES'][] = 'Contact successfully saved!';
		
		// go to home page
		return 'success';

The first line in the above will remove the contact from session. It is important we remember to do that, otherwise our session size could grow cluttered full of things we don't need consuming server memory when we don't have to. The second line will put a message in the session telling the user we saved the contact successfully. Now, don't worry we will be language conscious in Geeklog 2, this only for reference. The last line then instructs the controller what to do next. I know a lot of 'magic' seems to happen with the controller and we are about to explain how it all happens.

A Simple View << previous   Next >> Introduction to Models
Geeklog 2 Wiki Homepage