UserFrosting tries to keep up with the tools and best practices of the modern PHP community. If any of the tools and concepts discussed below are unfamiliar to you, don't worry! They're easy, and definitely worth it. If you are, feel free to skip those sections.
We also highly recommend that you check out PHP The Right Way. It does a good job explaining the major considerations for building clean, maintainable, and secure software in PHP, without pushing any particular framework down your throat.
UserFrosting builds on top of a number of existing components. After all, why reinvent the wheel if there is already a well-documented, well-tested solution? However, new versions of these dependencies are released all the time, and it can be difficult to keep up with changes.
To efficiently manage UserFrosting's dependencies, we use Composer. Composer is a dependency management tool that you can install locally on your development workstation. When run from the command line in your project directory, it consults a special schema file, composer.json
, and downloads the dependencies defined in that file from a central repository called Packagist. By default and by convention, each component is placed in a separate subdirectory of the vendor
directory. UserFrosting's vendor
directory is located in the userfrosting
directory.
Composer also autoloads the files and classes from these packages. This means that instead of having a long list of require
statements in the config file, we only need to include one file, vendor/autoload.php
. This file is automatically generated by Composer by scanning the package contents. Composer can also autoload the files and classes that are specific to UserFrosting. When you run composer update
, Composer will scan the controllers
, middleware
, and models
directories for new files and classes.
For your convenience, the latest versions of UserFrosting's dependencies are already included in this repository. However, if you choose to use UserFrosting for your own project, you will likely want to do one or more of these things:
In this case, you will need to install Composer.
To install composer on a Mac or other Unix-like operating system, visit install it. It is recommended that you install it globally.
To add additional dependencies, you will need to modify the userfrosting/composer.json
file. After this, run composer update
in the userfrosting
subdirectory to install the new dependencies.
All dependencies are installed in userfrosting/vendor
. Do not manually change the contents of this directory! The contents of this directory are automatically managed by Composer.
Libraries which have been installed with composer are autoloaded, so there is no need to include individual files. All you need is the vendor/autoload.php
file, which is already included in userfrosting/config-userfrosting.php
. See the "Configuration" section for more information on the config file.
If you're coming from the 0.1.x or 0.2.x versions of UserFrosting, you've probably noticed that the flow of the code has changed substantially. In particular, we now use a front controller pattern, also known as a URL router, which creates a layer of abstraction between the URL that you visit (for example, http://mysite.com/dashboard) and the code that gets run.
If you're new to PHP, you've probably been using the one-url-one-file scheme. This means that, in the document root of the filesystem on your server (for example, /~alexw/dev/htdocs/
), you create .php
files that correspond to the URLs that users of your site can visit. So, you might have a file /~alexw/dev/htdocs/command-center.php
, and then you visit http://localhost/command-center.php
to see the output of this script.
But here's the deal: there's no law set in stone that says it has to work this way. When you visit a URL, all you're really doing is placing an HTTP GET
request to a server (e.g., Apache). The request is basically asking the server to generate the appropriate response to that request. In the case of your typical home setup with Apache and PHP, the default behavior for a request is to look for a PHP script with the same name (command-center.php
) in some preconfigured document root directory, run it, and send its output back as the response, where it is displayed in the client's browser.
However, it is possible to configure the server to interpret requests differently - this is known as routing. Why would you want to do this? Because it gives you more flexibility. Let's say you want your site to have a URL like http://mysite.com/blog/2015-06-01/1
, which points to the first page of your blog posts from June 1. Without routing, you'd need to have actual subdirectories on your server's filesystem - /~alexw/dev/htdocs/blog/2015-06-01/1.php
.
What's wrong with this? Well, let's say you want the same blog post to also appear at other URLs, for example http://mysite.com/blog/rants
and http://mysite.com/blog/favorites/1
. You'd need to have these subdirectories as well, and you'd have to create actual scripts that output the same content. This becomes even more problematic if you want dynamically generated URLs, like http://mysite.com/blog/words-from-a-database
.
On the other hand, with a front controller, you can link URLs to specific pieces of code without needing to create a separate file.
UserFrosting uses the Slim Framework to make this work. Here's how:
http://mysite.com/users/u/1
, they are actually seeing a rewritten URL. In reality, every request is sent to index.php
, with users/u/1
sent as a request parameter. In Apache, this is done with an .htaccess
file (a preconfigured .htaccess
file is included with UserFrosting). Other web server technologies may use a different type of configuration file.index.php
, a number of routes are defined, which tell it how to respond based on the request parameter. Slim provides the framework for handling these routes. For example:$app->get('/users/u/1/?', function () use ($app) {
echo "Hello I am user number 1";
});
generates the output for http://mysite.com/users/u/1
. $app
is the Slim application (a global variable), and get
tells us that we are dealing with a GET
request (any time you navigate to a URL in your browser, you are submitting a GET
request).
Another advantage is that routes can have variables in them. For example:
$app->get('/users/u/:user_id/?', function ($user_id) use ($app) {
echo "Hello I am user number $user_id";
});
Now we can visit any URL, for example http://mysite.com/users/u/27
, and we will get the corresponding output "Hello I am user number 27."
The code for UserFrosting is divided into two main folders - public
and userfrosting
. public
is the directory that faces the world; any file that a visitor to your site can directly access is in this directory. Everything else is in userfrosting
.
/
|-- public // This directory should be publicly accessible (usually permissions set to 750 or 755)
| |
| |-- css
| |-- images
| |-- js
| |-- .htaccess
| |-- index.php
|
|-- userfrosting // This directory should only be accessible by the server (usually set permissions to 700)
| |-- controllers
| |-- locale
| |-- middleware
| |-- models
| |-- plugins
| |-- schema
| |-- templates
| |-- vendor
| |-- composer.json
| |-- composer.lock
| |-- config-userfrosting.php
If you're new to URL routing and the front controller pattern, you may be wondering - how can everything be accessible from public
when I only see one.php
file in there?
Don't panic - this is how it's supposed to work. The content and behavior of each page are no longer locked up into individual .php
files. Instead, all URLs are redirected to index.php
(via the .htaccess
file if you're using Apache). index.php
then calls the appropriate controller, which generates the appropriate content for the requested page.
For example, here is the router code in index.php
that handles the user management page (http://yoursite.com/users
):
$app->get('/users/?', function () use ($app) {
$controller = new UF\UserController($app);
return $controller->pageUsers();
});
$app
is the global Slim object. The get
method defines a route, which is a URL that your website can accept and generate a response to. Any time you type a URL into your browser, you are submitting a GET
request to the web server.
The line
$controller = new UF\UserController($app);
tells us to create a new UserController
object, which is responsible for mediating the interaction between what the client sees (the view) and the data and methods that constitute the user accounts (the model).
All controller classes for UserFrosting are located in userfrosting/controllers/
.
Think of index.php
as a receptionist at a big company. The various pages that your site's visitors navigate to are the employees. When a client wants to visit an employee, they don't go straight to their office - instead, they see the receptionist, tell him where they want to go, get checked in, and then get passed along to a security escort. The security escort controls their interaction with the members and resources of the company.
UserController::pageUsers()
is the method responsible for mediating this particular interaction, namely, the rendering of a table of users as HTML. By convention, any controller method that generates a complete HTML document is prefixed with page
.
Now, let's look at the code for the pageUsers
function, which is in userfrosting/controllers/UserController.php
:
/**
* Renders the user listing page.
*
* This page renders a table of users, with dropdown menus for admin actions for each user.
* Actions typically include: edit user details, activate user, enable/disable user, delete user.
* This page requires authentication.
* Request type: GET
* @param string $primary_group_name optional. If specified, will only display users in that particular primary group.
* @param bool $paginate_server_side optional. Set to true if you want UF to load each page of results via AJAX on demand, rather than all at once.
* @todo implement interface to modify user-assigned authorization hooks and permissions
*/
public function pageUsers($primary_group_name = null, $paginate_server_side = true)
{
// Optional filtering by primary group
if ($primary_group_name) {
$primary_group = Group::where('name', $primary_group_name)->first();
if (!$primary_group)
$this->_app->notFound();
// Access-controlled page
if (!$this->_app->user->checkAccess('uri_group_users', ['primary_group_id' => $primary_group->id])) {
$this->_app->notFound();
}
if (!$paginate_server_side) {
$user_collection = User::where('primary_group_id', $primary_group->id)->get();
$user_collection->getRecentEvents('sign_in');
$user_collection->getRecentEvents('sign_up', 'sign_up_time');
}
$name = $primary_group->name;
$icon = $primary_group->icon;
} else {
// Access-controlled page
if (!$this->_app->user->checkAccess('uri_users')) {
$this->_app->notFound();
}
if (!$paginate_server_side) {
$user_collection = User::get();
$user_collection->getRecentEvents('sign_in');
$user_collection->getRecentEvents('sign_up', 'sign_up_time');
}
$name = "Users";
$icon = "fa fa-users";
}
$this->_app->render('users/users.twig', [
"box_title" => $name,
"icon" => $icon,
"primary_group_name" => $primary_group_name,
"paginate_server_side" => $paginate_server_side,
"users" => isset($user_collection) ? $user_collection->toArray() : []
]);
}
This controller method can actually accept two kinds of requests: a request to show a table of all users, or a request to show a table of users from a single primary group. To keep it simple for now, let's just look at the part after the else
statement, which loads the table for all users:
// Access-controlled page
if (!$this->_app->user->checkAccess('uri_users')) {
$this->_app->notFound();
}
if (!$paginate_server_side) {
$user_collection = User::get();
$user_collection->getRecentEvents('sign_in');
$user_collection->getRecentEvents('sign_up', 'sign_up_time');
}
$name = "Users";
$icon = "fa fa-users";
First, we check to make sure that the current user has permission to access this content. $this->_app->user
references the currently logged-in user, and checkAccess
consults the database to determine whether the user has permission to access a certain resource. Resources are indicated by authorization hooks - in this case, uri_users
. The database contains a set of authorization rules, which map authorization hooks to users and groups.
Next, we load the collection of all users, using the Eloquent ORM. The User::get()
method returns a collection of User
objects. This collection is an instance of a special class that acts like an array (we can iterate over it), but can be extended with other methods for additional functionality. For example, the getRecentEvents
method fetches recent events for each User
in the collection. Internally, this is done by joining the user
and user_event
tables - but we don't have to get our hands messy with SQL statements!
Once we've loaded the data we want, we set some other dynamic parameters for the page we're about to render, and then pass everything into the $this->_app->render
method as an array:
$this->_app->render('users/users.twig', [
"box_title" => $name,
"icon" => $icon,
"primary_group_name" => $primary_group_name,
"paginate_server_side" => $paginate_server_side,
"users" => isset($user_collection) ? $user_collection->toArray() : []
]);
The template file for this page, users.twig
, is a basically just an HTML file that supports dynamically generated content via Twig placeholders and tags. Twig is a templating engine that helps us to better separate the logic (which is the controller's responsibility) from the presentation (which is encapsulated by the Twig template). This particular template file can be found in userfrosting/templates/themes/default/users/
.
In addition to the parameters that we pass in via render
, Twig has access to a number of global parameters. These are defined using Twig's addGlobal
method in the setupTwig
and setupTwigUserVariables
methods in models/UserFrosting.php
.
After the call to render
, everything is handed off to Twig, which generates the actual HTML that is sent back to the user. You can read more about this in creating content with Twig. Congratulations! You've now seen the sequence of steps whereby UserFrosting generates the appropriate content for a particular GET
request.
If you're unfamiliar with the MVC paradigm, you may be used to seeing PHP code that looks like this:
echo "<table><tr><th>Username</th><th>Email</th></tr>";
$stmt = $db->prepare("SELECT * FROM users);
$stmt->execute($sqlVars);
while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo "<tr><td>$r['user_name']</td><td>$r['email']</td></tr>";
}
echo "</table>";
We have the code that loads user data from the database all mixed into the code that actually displays the page. This is commonly referred to as "spaghetti code" - the code twists and turns without any perceivable stucture. This works for smaller projects, but can create a maintainability nightmare as your project grows.
The solution? MVC.
The fundamental principle behind MVC (model-view-controller) architecture is separation of concerns. The idea is to have one layer of code (the model) responsible for your data model (interacting with the database, performing various manipulations, etc), and another layer (the view) for generating the content that clients use to interact with your application.
The controller basically does everything else, mediating the interactions between the model and the view.
So, how do we implement this separation? Well for the view, we use a system of templates. Templates basically look like traditional static HTML documents, but they contain placeholders and tags that allow the controller to inject dynamic content. Our templating engine, Twig, takes the data provided by the controller and uses it to render a specified template. This is done via the $this->_app->render
method.
For an in-depth example of how UserFrosting harnesses Twig to render your content, please see Lesson 1 - creating a new page.
UserFrosting supports themable content, which means that the layout and appearance can vary depending on who is logged in. UserFrosting ships with three default themes: default
, nyx
, and root
. These themes (and any new ones you create) can be associated with primary groups, and users will be assigned the theme for their primary group when they log in.
So for example, if you assign the theme nyx
to the group Hydralisks
, then any user whose primary group is Hydralisks
will load the nyx
theme when they log in. The exceptions are for the root user, who is always given the root
theme, and guest users, who always get the default
theme.
When UserFrosting wants to render a particular template, it first looks for the template file in the user's assigned theme. If it can't find the file there, it then looks in the default
theme.
This means that if you want some content to be available to all users, you should put your template into the default
theme. You can then customize it for different themes by overriding the file in other theme directories.
Every theme also has a theme stylesheet, located in <theme-directory>/css/theme.css
. This stylesheet will be automatically loaded for every page.