$ cd /var/www/html $ tar zxvf ~/pronto.tar.gz $ mv pronto-svn pronto $ mysqladmin -u root create pronto $ cat app/config/*.sql | mysql -u root pronto $ vi app/config/config.php $ firefox http://localhost/pronto/
Most of this manual will be long and boring, so for those of you who already know everything about everything, here are the barebones steps necessary to get Pronto up and running.
We'll assume you're using MySQL and Apache. Also, make sure AllowOverrides is enabled for your Pronto directory, so Apache will honor the .htaccess files within.
Untar the Pronto tarball into your web root or a subdirectory thereof. We'll assume /var/www/html/pronto.
Create a database for Pronto, then create the necessary tables from the schema files in app/config/sql.
Edit app/config/config.php and change the DB_* constants near the top to match the access credentials for your new Pronto database. Then change the DIR_*_BASE constants to match the location of your Pronto installation.
Point your browser to the Pronto directory, eg, http://localhost/pronto/.
That's it! You should see a Pronto web page staring you in the face.
$ cd /var/www/html $ tar zxvf ~/pronto.tar.gz $ mv pronto-svn pronto $ mysqladmin -u root create pronto $ cat app/config/*.sql | mysql -u root pronto $ vi app/config/config.php $ firefox http://localhost/pronto/
Like most web frameworks, Pronto is designed to ease the development of web applications by eliminating redundant code and promoting a logical separation of application logic, data/model logic, and presentation.
Pronto strives to maintain a loosely-coupled set of components that, when combined, form a a powerful web development stack. The reasoning behind our "loosely-coupled" mandate is that this encourages Pronto to assist the developer, not take over for her. If a developer prefers to forgo some of the facilities provided by Pronto, it should be relatively easy for her to do so, without having to step out of the framework entirely.
There are four core elements to Pronto.
Dispatcher
Page Controller
Model
Template
Pronto also has plugin support for some layers.
Page Plugins
Template Plugins
Underneath the four high-level components, there are additional elements that support the framework.
DB Abstraction (with protection for SQL injection)
Data Sanitization (protection for XSS attacks)
Access Control
Internationalization (I18N)
Registry for global object storage
Factory for creating new objects
The following diagram outlines the typical flow of execution when a web request is received:
Entry script Dispatcher --> Page Controller --> Template
(index.php) ^ | ^ |
| | | | |
| | v | v
+--> Execution Profile Data Model Web Browser
The dispatcher is the traffic cop. It examines the request URI and decides which page controller it should pass control to, based on a series of regular expressions that define the URL->Controller mapping. The URL map is set in app/config/urls.php.
There is exactly one dispatch object at any point in the framework, and it is typically referred to as $web. The dispatcher receives control from the web execution profile and is charged with the task of determining which page controller it should pass control to.
The dispatcher also provides many lower-level facilities to controllers and plugins, such as:
context (information about the web request)
non-standard status code messages (403, 404)
an error handler (typically only used in debug mode)
http header control
javascript execution queueing
debug messages
relative->absolute url creation and parsing
template variable setting/getting
Despite the numerous facilities the dispatcher provides, its core mission is to direct control to the proper page controller. This is configured using an array that maps URL patterns to their respective page controllers.
$URLS = array( '/user/(.*)' => 'User', '/book/(.*)' => 'Book', '/login/' => array('User','login'), '/logout/' => array('User','logout'), '/' => 'Home' );
As the example suggests, each key in the $URLS associative array is a regular expression, and each value in the array is either the name of a specific controller class (string) or the name of a specific action within a specific controller class (array).
When routing a request, the dispatcher will choose the first array element that matches the current URL being requested. If the target is an entire controller (such as the /user/(.*) and /book(.*) examples above) then the portion of the regular expression surrounded by parentheses will be used to determine the action handler to be called within the page controller.
For example, if the browser issues a GET request to /user/list then the dispatcher will match the first rule in the $URLS array. It will then see that the list portion of the URL is the action handler requested, and will dispatch a request to pUser::GET_list().
If the action handler requested does not exist in the class, then the dispatcher will look for a "catch-all" handler for that request type. For example, if the browser issues a POST request for /user/list and the dispatcher cannot find a pUser::POST_list() method, then it will look for a pUser::POST() method instead.
The page controller is where most of your application logic will go. They are typically organized by the data entities they operate on, or a common theme of functionality. For example, one page controller may be responsible for allowing a user to register an account, login, change his/her password, etc. Another page controller may be used to manipulate blog posts, post comments, or both.
A page controller will receive control from the dispatcher and is responsible for a few things:
processing GET/POST input variables
interacting with data models
setting template variables
rendering templates or redirecting to new URLs
Page controllers are located in the app/pages directory. Each controller class name will be prefixed with a lowercase p, followed by the name of the controller itself. The file itself can technically be named anything you like, but conventionally it shares the same name as the controller. For example, the User page would have a class name of pUser and would be located in app/pages/user.php.
Within each page controller are a number of methods called action handlers. These methods are responsible for performing the logic required for the request(s) they are linked to.
To elucidate, let's begin with a simple example of a page controller.
<?php class pUser extends Page { function GET_hello() { $this->tset('greeting', 'Hello World! You issued a GET request.'); $this->render('user/hello.php'); } function POST_hello() { $this->tset('greeting', 'Hello World! You issued a POST request.'); $this->render('user/hello.php'); } } ?>
Notice that action handler methods are prefixed with the type of HTTP request that was issued. This separation concept is adopted from the WebPY framework and its subsequent PHP clone, WebPHP.
While at first glance this may seem tedious, it actually proves to be a very effective way of logically separating your processes based on the type of request. Take a minute and think about the typical uses for a GET and POST, especially when handling form data. The GET request is responsible for GETting the record, populating the form, and showing it to the visitor. The POST request receives input back from the user and is in charge of validating and possibly updating the record.
There have been many PHP projects and frameworks that will use a GET/POST variable such as $op or $action to denote the current mode being requested (eg, if($op == 'edit') {} else if($op == 'update') {}).
By splitting our logic into separate methods, we avoid the cumbersome $action variable entirely. As a bonus, we can easily hook into other, lesser-used HTTP verbs if need be. For example, if you're writing an RSS-friendly blog, you may want to implement a HEAD request for your blog, as some readers will use this to retrieve the Last-Modified header or the E-Tag header. Or perhaps you'd like to write a RESTful API and need to leverage the PUT and DELETE verbs.
There are a few ways of collecting GET/POST data from within a controller. The first method is through the use of a couple convenience methods in the base Page class.
<?php class pUser extends Page { function GET_hello() { $name = $this->param('name', 'Joe'); $this->tset('recipient', $name); $this->render('user/hello.php'); } } ?>
Page::param() is the quickest way to grab a specific parameter from the $_REQUEST array (which is a prioritized combination of GET and POST variables, POST taking precedence). If the variable requested is blank or does not exist, then the second parameter will be used as a default.
If you'd like to load the entire GET/POST data set into an associative array, then you can use Page::load_input().
<?php class pUser extends Page { function GET_hello() { $data = $this->load_input(); $this->tset('recipient', $data['name']); $this->render('user/hello.php'); } } ?>
Of course, you're free to access variables directly through the $_GET and $_POST superglobals, but Pronto will take care of any magic_quotes nonsense if you go through the Page class.
The Page::param() and Page::load_input() methods are useful when accessing standard GET/POST variables. But many applications today prefer to embed some variables inline. In other words, they appear in the URL itself, and not in the query string following.
For example, /user/edit?id=3 would become /user/edit/3.
To handle situations like these (and more), Pronto has introduced the concept of wildcard action handlers. These function like a typical action handler, except they can be called when only the beginning of the URL request matches the regexp in the $URLS routing table.
Once again, an example will elucidate.
<?php class pUser extends Page { function GET_hello__($name='Joe') { $this->tset('recipient', $name); $this->render('user/hello.php'); } } ?>
Notice the two underscores trailing the method name. This denotes the action handler as a wildcard one. It means that, if an exact match cannot be found for the request (in this case, GET_hello()) then the dispatcher will look for a wildcard handler that matches. If it finds one, everything trailing after the matching portion will be treated as a variable, and passed into the action handler as such. So if the browser calls /user/hello/john, the dispatcher will call pUser::GET_hello__('john').
If a specific action cannot be found and a wildcard action cannot be found, then the dispatcher will default to the "catch-all" handler for that request type. In this case it would be GET(). If the catch-all does not exist either, then a 404 is issued.
|
Note
|
It is important to provide a default value for any inline variable arguments in your action handler's method prototype. If you don't, then a visitor could inadvertently trigger an error if they made a request that did not include the variable (eg, /user/hello/). |
It's very important to validate all data coming in from the web browser, both to ensure data integrity and maintain security.
Most validation is delegated to the data models, but there are times when it's necessary to validate some data that isn't destined for a data model. In these cases, you can validate the directory from within the page controller.
The class in charge of data validation is predictably called Validator. There are shortcut methods in the Page class, so the validation methods below can also be accessed through the shortcuts.
| Validator::required() | Check a number of _REQUEST parameters to ensure that all are present and non-empty |
| Validator::validate() | Validate a _REQUEST parameter against a regular expression |
| Validator::is_valid() | Same as Page::validate() but just return a true/false, don't populate the $errors array if validation fails |
When using Validator::validate() and Validator::is_valid(), you can pass in any Perl-compatible regular expression. You can also use one of the regular expressions defined at the top of pronto/core/validator.php.
<?php class pUser extends Page { function POST_save() { $errors = array(); // We use the shortcut methods here, though we could // alternately call them through $this->validator. Same thing. $this->required($errors, array('name','address')); $this->validate($errors, 'email', VALID_EMAIL, 'Invalid email address'); $this->validate($errors, 'age', VALID_NUMBER, 'Please provide a valid integer'); } } ?>
The typical flow of a Pronto web request usually only involves a single page controller, but there are some scenarios where it makes sense to call an action from another page controller. This is possible with the Page::render_element() method.
Most methods in a page controller start with an HTTP verb, such as GET_, POST_, or the lesser-used PUT_, DELETE_, etc. Page controller can also provide elements which are methods intended to be called from other page controllers. These methods must have a prefix of ELEM_.
<?php class pBlog extends Page { function ELEM_view($user_id) { $this->tset('blog_data', $this->models->blog->get_by('user_id', $user_id)); // render the blog -- this template could be just an HTML // snippet that will be inserted into a full template by // the caller $this->render('blog/elem.view.php'); } } ?>
<?php class pUser extends Page { function GET_profile() { $id = $this->param('user_id'); $blog = $this->render_element('Blog', 'view', array($id)); $this->tset('blog_content', $blog); $this->render('user/profile.php'); } } ?>
Templates serve as the presentation layer of Pronto. When a page controller has finished its business, it will typically render a template via the Page::render() or Page::ajax_render() methods.
Templates are unique to Pronto, in that they aren't classes themselves, and they don't have access to other areas of Pronto, such as the dispatcher, controllers, models, or the database ($db). However, template files are managed by the Template class, which is ultimately in charge of setting/getting template variables, as well as the fetching/processing of templates themselves.
Pronto's template system is rather standard. You can use plain HTML and PHP as you wish. However, the only variables accessible are those set by the page controller via the Template::set() method.
Templates can also use any active template plugins, also called helpers. These will be covered later, though the example below makes use of the $html helper.
<p>Hello, <?php echo $name ?>!</p> <p>I am a template.</p> <p><?php echo $html->link('Go home', '/') ?></p>
Templates are usually organized by page controller, since that's who typically renders them. The logical structure is to have subdirectories within the app/templates directory that match the names of the page controllers that will be rendering them. This is recommended but not necessary.
As shown in the example above, templates can reference their variables in the global scope, but they are limited only to variables that have been set using Template::set(), Page::tset(), or objects that serve as template plugins.
The Template class offers methods for setting, getting, and testing the existence of template variables.
|
Note
|
While Template::set() is the real variable setter, you're also free to use Page::tset() which is a shortcut method to the real one. Likewise, there are shortcuts for Template::get(), Template::is_set(), and Template::un_set(). |
<?php class pUser extends Page { function GET() { // set it... $this->template->set('name', 'Joe'); // get it back... $name = $this->template->get('name'; // has it been set? if($this->template->is_set('name')) { // then unset it $this->template->un_set('name'); } // alternately, we can use the shortcuts... $this->tset('name', 'John'); $name = $this->tget('name'); } } ?>
There are three functions that can be used to render templates.
| Page::render() | Parse a template and render it to the browser |
| Page::fetch() | Parse a template and return the output to the caller |
| Page::ajax_render() | Render a template as an AJAX response (covered more later) |
You'll probably use the Page::render() function the most, but Page::fetch() can be useful if you're combining templates, or rendering them somewhere other than the browser (eg, email). Both functions allow you pass in additional template variables that are not set globally via Template::set() or Page::tset().
<?php class pUser extends Page { function GET() { $this->set('name', 'Joe'); // this path is relative the app/templates directory $this->render('user/hello.php', array('age'=>27)); } } ?>
Like the template methods, the real functionality is actually in the Template class itself. The methods in Page are merely convenience methods that call the real ones.
Layouts provide a way of establishing one or more outer structures to your web application. Most of the application's structure and styling will be relegated to the layout, leaving the core content in the templates themselves. While not considered a regular template, layouts still have access to template variables and plugins.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=<?php echo CHARSET ?>"> <title><?php echo SITE_NAME ?></title> <?php echo $html->favicon('favicon') ?> <?php echo $html->css('main') ?> <?php echo $html->js('jq/jquery') ?> <?php echo $HTML_HEAD ?> </head> <body> <div id="header"> Super Cool Web App v1.0 </div> <div id="content"> <?php echo $CONTENT_FOR_LAYOUT ?> </div> </body> </html>
As you can see, all the "outer" stuff gets thrown in the layout. When a template is rendered within a layout, it will be substituted for the variable $CONTENT_FOR_LAYOUT. The other special layout variable is $HTML_HEAD, a placeholder variable for any additional tags that may need to be inserted into the <head> of your document.
Multiple layouts are supported. You can bind to a specific layout with the Page::render() method, or you can set a layout in a page controller's __init()__ method.
<?php class pUser extends Page { function __init__() { // set a different layout for this page. // path is relative to app/templates. $this->set_layout('layout.other.php'); } function GET() { $this->set('name', 'Joe'); // render this template in a specific layout $this->render('user/hello.php', array(), 'layout.new.php'); } } ?>
Models are responsible for any logic that surrounds the manipulation of a data entity. Its typical responsibilities are to faciliate create, retrieve, update and delete (CRUD) operations. If more advanced manipulation/processing functions are required for an entity, they will also be located here.
Models do not actually perform any data introspection or Object Relational Mapping (ORM) by default. They simply act as a place to store functions that relate directly to a data entity. You are still responsible for writing your own SQL for the most part. The creation and execution of safe SQL is reinforced by a well-designed database abstraction class that focuses on the DRY (Don't Repeat Yourself) principle.
While Pronto does not provide any full ORM capabilities, the base Model class does provide some convience functions for the more simple queries, such as PK-based lookups or lookups with a single column in the WHERE clause.
|
Note
|
If you absolutely love ORM, then Pronto may not be for you, at least not at this time. But if you're not head over heels, then give it a shot. You may find that well-organized SQL queries are almost as quick as defining all your belongs-to and has-many relations in your ORM classes. |
The base Model class provides an interface for the common operations associated with a simple data entity.
| validate_for_insert | Validate form data for an insert operation |
| validate_for_update | Validate form data for an update operation |
| sanitize | Perform any necessary data sanitization prior to operating |
| create | Return a "default" set of data to populate a form for entity creation |
| get | Return a full data record, retrieved by Primary Key (PK) |
| get_record | Do the actual retrieval of a data record (see NOTE below) |
| delete | Delete a record by PK |
| insert | Insert a new record |
| update | Update an existing record |
| list_params | Provide parameters necessary to generate a |
| comprehensive "list" of records |
|
Note
|
You'll notice that models use both get() and get_record(). get() is the method that should be used by controllers and other models to fetch an entity through this model. But you should never actually override the get() method in your model, only the get_record() method. This is because the get() method actually uses get_record() to fetch the data, while wrapping it in some logic that employs full record caching. |
Depending on your data entity, some or all of these functions may not be relevant. If you are using the specialized controller Page_CRUD, then it will expect these functions to be complete.
For extremely simple entites, the implementation within the base Model class will probably suffice. For all others, you will want to override the functionality within your own model class.
Model class names are prefixed with a lowercase m.
The gatekeeper to all database access is the $db object. Like $web, it is available pretty much everywhere (either through $web or directly), though it is typically most used within data models.
The DB class provides some simple convenience methods for selects, inserts, and updates, while using the third-party SafeSQL library for parameter substitution and protection against SQL injection attacks.
An example is below, though you'll want to consult the API documentation for a full list of methods available.
<?php class mUser extends Model { function do_stuff($id) { // get a full record... $data = $this->db->get_item("SELECT * FROM users WHERE id=%i", array($id)); // this works too... $data = $this->db->get_item_by_pk('users', 'id', $id); if($data) { // update it with new data... $data['last_login'] = date('Y-m-d'); $this->db->update_row('users', $data, 'id=%i', array($id)); } // count users who logged in today... $ct = $this->db->get_value("SELECT COUNT(*) FROM users WHERE last_login=CURDATE()"); return $ct; } } ?>
As you can see, you can use placeholders within your SQL queries to denote variables that can be substituted by SafeSQL when it's time to execute the query. This is the recommended method of using variables in your queries, as SafeSQL can ensure that any malicious SQL (or other poorly-formed data) is properly escaped before being passed to the database engine.
There are different types of placeholders you can use, depending on what type of data you'll be passing in. If the variable type does not match the placeholder, SafeSQL will attempt to cast it.
| %s | string |
| %i | integer |
| %f | float |
| %c | comma-separated list, each element casted to integer |
| %l | comma-separated list, no quotes or casting |
| %q | comma-separated list, each element quoted (string) |
| %n | wrap the value in quotes unless it is NULL |
In a well-organized application, almost all of your DB work will be in the models themselves. But just in case, $db is available to the dispatch and page controllers. It can be passed to plugins as well, though there are usually better ways of doing this.
Of all the operations listed in the beginning of this section, the one that probably stands out is the last one, Model::list_params(). Within the Pronto framework itself, this method is only used by one method, and that is Page_CRUD::GET_list(). The Page_CRUD class will be covered later, but Model::list_params() can be used by application code that you write as well, so we'll cover it here.
The purpose of Model::list_params() is to return the information required to construct a SQL query that will return a useful dataset to populate a record listing.
That's a bit of a mouthful, so perhaps a well-contrived example will serve us better.
Let's say you need to show a tabled list of records for a data entity. In a perfect world, you could just issue a "SELECT * FROM $table" and pass it to a template plugin that generates the table. But what if some columns are not mapped directly to a column in your table? What if you need to join against other tables, or restrict your data with a WHERE clause?
All of this is a cinch to do with SQL (that's what it was designed for, after all), but if a plugin or controller action is in charge of deciding which field to sort by or which data to restrict, then it can be easier to start by separating your query into its logical segments. That's what Model::list_params() does.
Model::list_params() returns an associative array, that contains six elements.
|
Note
|
Like the rest of the Model interface, you only need to implement this method if you're actually going to use it. |
| from | string | The FROM clause that will be used. This should be well-formed SQL. |
| exprs | array | Zero or more SQL expressions that will be used in the SELECT clause of the query. These are typically pieces of data that aren't regular data columns, but are passed through some SQL functions or similar. |
| select | string | The main SELECT clause of the query. Can be as simple as * or you can select certain columns (eg, u.id,u.name). |
| where | array | An array of elements that will construct your WHERE clause. These will be AND'ed together, along with any other filter data that might be added by your controller. (eg, array(status="active", confirmed=1)) |
| where_args | array | An array of arguments that will be substituted for any SafeSQL placeholders in the where array. |
| group_by | string | The columns to group by, if any. |
<?php class mPost extends Model { function list_params() { return array( 'from' => 'posts p INNER JOIN blogs b ON b.id=p.blog_id', 'exprs' => array('posted_at' => 'CONCAT(p.post_date," ",p.post_time)'), 'select' => 'p.*,b.name', 'where' => array('b.user_id=%i'), 'where_args' => array(ACCESS_ID), 'group_by' => '' ); } } ?>
If you'd like to see how this data is used, take a look at the Page_CRUD::GET_list() method in pronto/core/page_crud.php.
Let's look at a simple example of a model and how it might be used from a page controller.
<?php class mUser extends Model { // these are used by the CRUD record listing facility, and // can be used by any custom code you write as well. var $default_sort = 'last_name ASC'; var $per_page = 50; function validate_for_insert($data) { // all our errors will be returned in this array $errors = array(); // we can use the Page::validate() and Page::required() functions $this->page->required($errors, array('first_name','last_name','password')); $this->page->validate($errors, 'email', VALID_EMAIL, 'Valid email required'); // make sure passwords match if(!empty($data['password']) && $data['password'] != $data['password2']) { $errors['password2'] = 'Passwords do not match'; } // check for a duplicate username if($this->db->get_value("SELECT id FROM users WHERE email='%s'", array($data['email']))) { $errors['email'] = 'This email address is already in use.'; } return $errors; } function validate_for_update($data) { // similar to validate_for_insert, so we'll leave this out for // brevity... } function create() { // this method is used by the Page_CRUD::GET_create() method to set // some default values in the create form. However, we don't really // have any defaults to put in here... return array(); } function get_record($id) { $data = parent::get_record($id); // we don't need the password... unset($data['password']); return $data; } function insert($data) { // set some metadata... $data['created_on'] = date('Y-m-d'); $data['status'] = 'active'; // encrypt password $data['password'] = sha1($data['password']); // this will return the new insert ID return parent::insert($data); } function update($data) { // don't let the user change these fields... unset($data['status'], $data['created_on']); // encrypt password, if it was changed if(!empty($data['password'])) { $data['password'] = sha1($data['password']); } else { unset($data['password']); } return parent::update($data); } function delete($id) { // we employ lazy deletion... $this->db->execute("UPDATE users SET status='deleted' WHERE id=%i", array($id)); } } ?>
Now we can create a page controller that uses these methods. Our basic controller will provide a registration facility for new users.
<?php class pUser extends Page { function __init__() { // import the model so it can be accessed via $this->models $this->import_model('user'); } function GET_signup() { if(!$this->tisset('data')) { // no form data is set yet, give it some defaults $this->tset('data', $this->models->user->create()); } $this->render('user/signup.php'); } function POST_signup() { // grab the POST data into an associative array $data = $this->load_input(); // validate $errors = $this->models->user->validate_for_insert($data); if(!empty($errors)) { // validation failed... return the visitor to the signup // form... The Page class provides a mechanism for this. It will // pass the form data and the errors back through the session and // issue a redirect to the signup form. $this->return_to_form('/signup', $data, $errors); return; } // validation passed, do the insert $user_id = $this->models->user->insert($data); // now send them to their profile page $this->redirect("/profile?id=$user_id"); } } ?>
As you can see, the separation of data logic and controller logic makes for some well-organized, readable code, and the GET/POST split in the controller makes it easy to tell which stage we're at in the signup process.
It's possible for a model to depend on other models. This occurs a lot when you have entities that link to each other. A model for managing users probably shouldn't directly modify another entity type, so instead it can ask the respective model to do the work on its behalf.
<?php class mUser extends Model { function delete($id) { // load the Blog model as a dependency so we can delete this user's // blog record as well. $this->depend('blog'); $user = $this->get($id); $this->depends->blog->delete($user['blog_id']); parent::delete($id); } } ?>
There is no special facility in place for storing and retrieving session data, so you can use the standard PHP superglobal $_SESSION just as you would in a basic PHP script.
<?php $_SESSION['rocket_science'] = false; $_SESSION['other_stuff'] = 'Pretty basic... why complicate things?'; ?>
Likewise, there is no policy on authentication either, though Pronto does provide a facility for access control lists (ACLs) and access checks.
What this means is that you're free to authenticate your users however you like. Once authenticated, you can use Pronto's access control features to keep track of whether a user is logged in or not, and what areas of the web application they are allowed to access.
In Pronto, there are two pieces of data that constitute your access policy framework: The access model and the access keys.
An access key, when assigned to a user, gives that user access to parts of the site that require it. The access model determines how the access key structure will be interpreted and used.
There are two primary access models in Pronto: roles and discrete. In both models, your access keys will be an associative array of keys and values.
|
Note
|
Unfortunately, the term key is used for two meanings here: as an access key, and as an array key. Pay close attention to which one we're talking about. |
| roles | Within your access key set, each array key is a role or group name, and the array value is an array of keys (strings) that will be assigned to that role. A user can be assigned to zero or more roles. The role names themselves should be stored in the DB record for the user. At login time, they will be resolved to the individual access keys and assigned. |
| discrete | The structure of your access key set is the same as in the roles model. However, in this model, the actual keys themselves should be assigned to the user within the DB, so you can pick and choose which keys from which modules get assigned. This provides a higher level of control for each user, but is slightly more complicated to conceptualize and implement. |
If you're not sure which one to use, start with the roles model. Conceptually, it is simpler, analagous to assigning a "rank" to each user in the system.
Both the access model and the access_keys are configured in app/config/access.php.
<?php define('ACCESS_MODEL', 'roles'); // Though the roles model can support each user having more than one role, // our application will only assign one role per user. If the user's // record says they are a "User", then we assign them the 'USER' access // key. If they're marked as an "Administrator", then we assign them both // the 'USER' and the 'ADMIN' access keys. $ACCESS_KEYS = array( 'User' => array('USER'), 'Administrator' => array('USER','ADMIN') ); ?>
To protect your controllers (or actions within) against unauthorized access, you can use the Web::check_access() method.
Also, once a user is logged in, Pronto's access layer will set the constant ACCESS_ID to the unique ID of that user. You can use this constant throughout the web application to check if a user is logged in (regardless of access keys assigned) and to link entities to the logged-in user.
<?php class pUser extends Page { function __init__() { $this->import_model('user'); } function GET_edit_profile() { // check that this person has the 'USER' key... if not, then one of // two things will happen: // 1. if the user isn't logged in, they'll be sent to the login page // 2. if the user is logged in but doesn't have this key, they will // see a "403 Forbidden" page. // $this->web->check_access('USER'); // okay, they're logged in and they have the 'USER' access key, so // grab their profile data $profile = $this->models->user->get(ACCESS_ID); $this->tset('profile', $profile); $this->render('user/edit_profile.php'); } } ?>
You can also use Web::has_access() if you merely want to see if the user has valid access, but not act on the result. There is also a global shortcut function to Web::has_access() called a(), which can be used anywhere in Pronto, even where $web is not accessible.
<?php class pUser extends Page { function GET_stuff() { if($this->web->has_access('USER')) { echo "You are allowed"; } // this would do the same if(a('USER')) { echo "You are allowed"; } } ?>
So now that we know how to test for proper access, how do we tell Pronto that a user is authenticated?
The $web object holds a reference to the Access class, which is the brains of Pronto's access control facility. To set, get, or clear keys, we go through $web->access.
Let's add a couple methods to our mUser model class that can handle authentication.
<?php class mUser extends Model { /** * Returns true if the user is authenticated, else returns an error * string to be passed back to the browser. */ function authenticate($email, $password) { $user = $this->db->get_item("SELECT * FROM users WHERE email='%s'", array($email)); if(!$user) return 'Invalid email/password'; if($user['password'] != sha1($password)) return 'Invalid email/password'; if($user['status'] != 'active') return 'This account is not active'; // Okay, everything checks out, so assign the user's access id and // his/her keys... $this->web->access->set_id($user['id']); $this->web->access->set_keys($user['access_keys']); $this->db->execute("UPDATE users SET last_login=CURDATE() WHERE id=%i", array($user['id'])); // Now we'll store the user's record in the session so we don't have // to fetch it from the DB each time $_SESSION[