-
Notifications
You must be signed in to change notification settings - Fork 0
Build a Sample Form
``This is the main tutorial for the THINKer Framework. It may take up to 30 minutes to complete.
It assumes that you have a general knowledge of PHP and MySQL. You should already have XAMPP or another program installed to be your web server and database. I recommend installing MySQL Workbench for database design. Follow the instructions on the Installation page of this wiki if you haven't already.
Let's start off development by creating a form to register an Organization for THON. There are many steps involved in this process:
- Determine what data you need to collect
- Build the database table(s)
- Create the Model(s)
- Create a Controller to handle the data
- Create the components to display the form to a user
- Test the project
####1. Determine What Data You Need To Collect
The first step in any project of this type is figuring out exactly what you should be collecting. For our purposes, you need to grab the following items:
- Organization Name
- Organization Type
- This can be: Independent Couple, General, Special Interest, or Greek
- Primary Contact Name
- Primary Contact Email
- Primary Contact Phone Number
It's also always a good idea to record the date/time of when the submission was made, along with when it was last updated and also deleted, if necessary. We also want to have our own space to record our comments and whether or not we're accepting the submission.
####2. Build the Database Tables(s)
Before we do anything technical with the database, it's important to determine the proper data types for each of these fields. A list of the data collected and an explanation for the data type selected is below:
- Organization Name
- VARCHAR
- This is a simple string.
- Organization Type
- ENUM
- We only have a certain, select number of values this can be, all of which are strings. An ENUM makes sense here because it stores these types differently.
- Primary Contact Name
- VARCHAR
- This is also a string.
- Primary Contact Email
- VARCHAR
- Emails, while they all follow a similar format, are just stored as VARCHAR as well. There should be validation from the application to ensure the format is correct, but there's no way to do that in MySQL.
- Primary Contact Phone Number
- VARCHAR
- Many people will say that INT is fine, but this only works for US Phone Numbers. Yes, most of your applications may not need non-US phone numbers, but for good design and greater scalability, you should keep it as VARCHAR to also ensure that special characters like '+1' before the number are preserved. The characters you want to keep are entirely up to you, and again, you can verify the correct format in the application itself.
- Comments
- TEXT
- Comments could be lengthy, and the TEXT datatype is good for this in most cases since they should generally be short (not a novel). MEDIUMTEXT and BIGTEXT also exist for cases where you might be dealing with novels.
- Approved
- TINYINT
- Being approved is binary -- you are, or you aren't. TINYINT stores either a 0 or a 1, 0 being 'Off' or 'No', 1 being 'On' or 'Yes'. This is the most efficient data type for a simple Yes/No value. If you needed other values, an ENUM might be helpful.
- Date/Time of Submission
- DATETIME
- MySQL has its own type for dealing with dates and times, and DATETIME is that. If you just want a date, you can use DATE. The same for a time, which is just TIME. Note that this accepts whatever value you give it, so it is based off the timezone of your application.
- Date/Time of Last Update
- TIMESTAMP
- MySQL also has a data type that automatically updates the date/time contained in that column for individual rows when they're updated, without additional intervention from you. It works just like DATETIME with this advantage. However, you can only have one column per table with this data type, so if you need more of these then you'll have to manage that from the application. Also note that changes made directly to the database will update this column, not just from the application.
- Date/Time of Deletion
- DATETIME
- Optional, but I strongly recommend it as it helps preserve data, especially when it's been accidentally deleted. If you're not tight on space, add this.
Additionally, you'll want to include an ID column, which serves as the unique identifier for each entry (these are known as Primary Keys). This can just be an integer that automatically increments itself from the database's end (so there's no work needed on your end to do this). In larger applications or where there are expected to be an unusually high number of entries, a unique ID could be generated by the application following a certain pattern (such as a GUID), but that is not covered in this tutorial.
Now, we need to actually create the table. Each column has its own unique name, which should accurately describe its contents. For our purposes, we might create a table that looks like the following:
- id
- INT(11)
- AUTO_INCREMENT
- PRIMARY_KEY
- organization_name
- VARCHAR(255)
- organization_type
- ENUM('Independent Couple', 'General', 'Greek', 'Special Interest')
- primary_contact_name
- VARCHAR(255)
- primary_contact_email
- VARCHAR(255)
- primary_contact_phone
- VARCHAR(255)
- comments
- TEXT
- approved
- TINYINT(1)
- created_timestamp
- DATETIME
- updated_timestamp
- TIMESTAMP
- deleted_timestamp
- DATETIME
The value next to the data type is the maximum length. It's hard to anticipate exactly what the largest value you'll encounter will be, but overestimating is okay in most cases. The difference is just how the data itself is stored and you could be using additional disk space if you're overestimating too much. A well-designed application should put this into careful consideration.
#####Design Note
This is just a sample provided purely as a tutorial. In a larger, well-designed database, the users and organizations would be stored in separate tables as their own entities, and those entities' primary keys would be used instead of storing the Organization Name in this new table as text. This means our table would look more like this:
- id
- INT(11)
- AUTO_INCREMENT
- PRIMARY_KEY
- organization_name
- VARCHAR(255)
- organization_type
- ENUM('Independent Couple', 'General', 'Greek', 'Special Interest')
- primary_contact_username
- VARCHAR(255)
- This should technically match the data type in a 'users' table, and then a Foreign Key relationship would be specified on this column.
- Then, when writing queries, you add a JOIN so you can pull the user's email address and phone numbers (of course, this assumes there's already a corresponding value in the 'users' table).
- comments
- TEXT
- approved
- TINYINT(1)
- created_timestamp
- DATETIME
- updated_timestamp
- TIMESTAMP
- deleted_timestamp
- DATETIME
####3. Create the Model(s)
Models are very simple to create once you have a database table created, as they are just classes with private properties matching the names of each database column. The sample Model for OrganizationSignup is below:
<?php
/**
* models/OrganizationSignup.php
* OrganizationSignup Class
*
* @author Cory Gehr
*/
namespace SampleApp;
class OrganizationSignup extends \Thinker\Framework\Model
{
// Specify each field expected
private $id;
private $organization_name;
private $organization_type;
private $primary_contact_name;
private $primary_contact_email;
private $primary_contact_phone;
private $comments;
private $approved;
private $created_timestamp;
private $updated_timestamp;
private $deleted_timestamp;
`/**`
`* __construct()`
`* Constructor for the OrganizationSignup class`
`*`
`* @access public`
`* @param int $id ID of the Organization Signup Object`
`*/`
`public function __construct($id)`
`{`
`global $_DB;`
`// Ensure we were given an ID before we attempt to create the object`
`if($id)`
`{`
`// Query the database for the specified row`
`$query = "SELECT id, organization_name, organization_type, `
`primary_contact_name, primary_contact_email, `
`primary_contact_phone, comments, approved, `
`created_timestamp, updated_timestamp, `
`deleted_timestamp `
`FROM organization_signups `
`WHERE id = :id `
`LIMIT 1";`
`$params = array(':id' => $id);`
`$result = $_DB['database']->doQueryOne($query, $params);`
`if($result)`
`{`
`// Load data into object`
`// This is a short way to automatically load database columns `
`// into the proper local object`
`foreach($result as $name => $val)`
`{`
`$this->{$name} = $val;`
`}`
`}`
`else`
`{`
`trigger_error("Unable to load OrganizationSignup row with ID $id.");`
`}`
`}`
`else`
`{`
`trigger_error("No ID specified for the OrganizationSignup object to be instantiated from the database.");`
`}`
`}`
`/**`
`* create()`
`* Creates a new OrganizationSignup entry in the database`
`*`
`* @access public`
`* @static`
`* @param string $organization_name Name of the Organization`
`* @param string $organization_type Organization Type`
`* @param string $primary_contact_name Name of Organization Primary Contact`
`* @param string $primary_contact_email Email Address of Primary Contact`
`* @param string $primary_contact_phone Phone Number of Primary Contact`
`* @return int ID of the new OrganizationSignup entry`
`*/`
`public static function create($organization_name, $organization_type, $primary_contact_name, $primary_contact_email, $primary_contact_phone)`
`{`
`global $_DB;`
`$query = "INSERT INTO organization_signups(organization_name, `
`organization_type, primary_contact_name, `
`primary_contact_email, primary_contact_phone, `
`created_timestamp) `
`VALUES (:organization_name, :organization_type, `
`:primary_contact_name, :primary_contact_email, `
`:primary_contact_phone, NOW())";`
`$params = array(':organization_name' => $organization_name, ':organization_type' => `
`$organization_type, ':primary_contact_name' => $primary_contact_name, `
`':primary_contact_email' => $primary_contact_email, `
`':primary_contact_phone' => $primary_contact_phone);`
`if($_DB['database']->doQuery($query, $params))`
`{`
`return $_DB['database']->lastInsertId();`
`}`
`else`
`{`
`trigger_error("Unable to create the OrganizationSignup entry in the database.");`
`return false;`
`}`
`}`
`/**`
`* delete()`
`* Deletes an OrganizationSignup entry in the database`
`*`
`* @access public`
`* @static`
`* @param int $id OrganizationSignup ID`
`* @return boolean True on Success, False on Failure`
`*/`
`public static function delete($id)`
`{`
`global $_DB;`
`$query = "UPDATE organization_signups `
`SET deleted_timestamp = NOW() `
`WHERE id = :id `
`LIMIT 1":`
`$params = array(':id' => $id);`
`return $_DB['database']->doQuery($query, $params);`
`}`
}
?>
In most cases, you would then create 'get' and 'set' functions for each value (for example, 'getOrganizationName()' and 'setApproval()'), but THINKer implements the magic methods __get() and __set() so you don't need to do this. So, if I wanted to get the 'organization_name' column, I would call:
$signupObject->__get('organization_name');
Similarly, I would update the 'approved' value doing the following:
$signupObject->__set('approved', 1);
(These examples assume that $signupObject is an instantiated OrganizationSignup object)
Additionally, you'll need to create functions to handle creation, updating, and deleting these objects. To be consistent with RESTful API commands, these should be named in conjunction with those commands:
- GET (get())
- Retrieve all properties associated with an entity
- PUT (put())
- Updates one or more properties with an entity
- POST (post())
- Creates a new entity
- DELETE (delete())
- Removes an entity
####4. Create a Controller
From the MVC pattern, the Controller is where all of the actions fall together. If something needs to be created, the controller will kick off that action. Need to delete something? Controllers make that happen to! We call controllers 'Sections' in THINKer.
In object-orientation, a Section is just another class with methods. The methods in a Section class are called 'SubSection'. In terms of a web page, you can think of these SubSections as different pages themselves (but the presentation logic comes in the view).
Below is a sample controller:
set('message1', $NewObject->getMessage()); $newMessage = "This is a test of the THINKer Framework's object handling capabilities."; $NewObject->setMessage($newMessage); $this->set('message2', $NewObject->getMessage()); } /** * test() * Passes data back for the 'test' subsection * * @access public */ public function test() { } } ?>Here's what's going on in this controller:
- The defaultSubsection() function returns the name of the SubSection that should be loaded if none is specified in the URL (more on this later). Usually you'll just return a string here.
- info() is the first SubSection function. Here, we're creating an instance of a SampleObj model and calling its methods. $this->set() is a function used to pass data back to the View layer of the application. We can see how this works in the next section, but for now, all you need to know is that if you need to output a piece of data, you'll use this function.
- test() is another SubSection, but notice how there is nothing in the function. This page is purely HTML and doesn't require additional data or function to be displayed, however, the function still needs to be declared so the framework understands it's a valid call.
####5. Build the View Components
####6. Test the Project
When you've finished your controller, you can call it using the following URL pattern:
http://{yourBaseDomain}/?s=OrgSignup&su=add
Note that in Apache, you can do URL rewriting so that the URL could be more like the following:
http://{yourBaseDomain}/OrgSignup/add
But this isn't covered here.
ALWAYS ALWAYS ALWAYS test each part of the project thoroughly. The following sections should be followed for any feature added to your application:
#####Smoke Testing
Smoke Testing is simply using the application as a user would in order to find the obvious or non-obvious bugs. You can log bugs based on usability issues or technical issues (remember that if the user has a hard time using the system, it defeats the purpose of having it in the first place!). Consider the following:
- What are the edge cases?
- Edge cases are extreme examples of behavior that could break the system
- For example, what if a user needs a value that would fall outside the boundaries of the data type length?
- This more occurs in the thought process of creating the table, but it's something to consider. You can't really correct this at the time of submission so you just have to fix this as it comes up.
- Where could a user possibly screw something up?
- Have you compensated for this?
- Have you given them a way to correct this action?
- Sometimes this isn't necessary
#####Unit Testing
Additionally, you need to write unit tests for each function of your controller and model to ensure that functionality isn't broken when making changes in the future.
####Final Notes
In practice, this could work if you only plan on having a few small sign-up forms here and there, but over time, creating new Models and Controllers for different submission forms becomes tedious and can add a lot of crud to an application. The material presented in this tutorial is helpful to gain an understanding of the different features of THINKer, but if your aim is a bunch of submission forms then you're better off creating your own form builder (not covered in this wiki).
THINKer Framework + Libraries Copyright © Cory Gehr. All rights reserved.