You're reading our legacy documentation, the documentation for deprecated versions of UserFrosting. To read the docs for the current version of UserFrosting, visit learn.userfrosting.com.

    Navigating the Code

    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.

    Dependency Management with Composer

    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.

    Installing Composer

    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:

    • Install a package that isn't already included;
    • Update a package to a newer version;
    • Add your own files and class definitions to the core codebase.

    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.

    Front Controllers and the Slim Microframework

    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.

    How does UserFrosting make this happen?

    UserFrosting uses the Slim Framework to make this work. Here's how:

    1. When a user visits a URL such as 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.
    2. In 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."

    Code Structure and MVC

    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.

    Creating Content with Twig

    Template Files and Dynamic Content

    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.

    Theming

    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.