Weblegs

Simple and flexible web apps using super fun technologies. Enhanced with with Bootstrap.

View project on GitHub Download Weblegs (v3.5.0)

Great for prototyping ideas.

Front-end Website

Just the basics: home, about, contact and login. Add and remove pages as needed.

User System

Login system and reset password built in. There is also groups and permissions.

Back-end Admin

Setup with the basics and ready to be extended. Don't be shy, check it out.


You already know this stuff.

Here is a summary view of the project file structure.

project/
    ├── app/
    │   ├── lib/...
    │   ├── config.php
    ├── cli/
    │   ├── test/...
    │   ├── run
    ├── lib/
    │   ├── footprint/...
    │   ├── weblegs/...
    ├── www/
    │   ├── ...
    │   ├── .controller.php
    │   ├── .htaccess
    │   ├── .route.php
    │   ├── index.html
    │   ├── favicon.ico
    │   ├── ...
    ├── www-cache/...
    ├── mysql-db-structure.sql
    └── README.md

Installation:

  1. Download and unzip (or git clone) files into a directory of your choice.
  2. Create new mysql database and import the mysql-db-structure.sql file.
  3. Configure project settings via the app/config.php file. Make sure you supply MySQL credentials.
  4. Supply SMTP credentials if you want emails to send.
  5. Tell Apache to serve files from www/.

Gotchas:

Weblegs apps can run in either www or cli mode. Web requests start at Apache and the cli scripts are executed on the command line (i.e.: cron jobs).

Both modes use the same config file and in either mode you have access to the Footprint Engine (lib/footprint/Engine.php). You can access this static object in any PHP scripts using the shorthand symbold F.

//set some config data (in either mode)
F::$config->set("user_id", 840);

//get some config data (in either mode)
F::$config->get("user_id");

Both modes follow an event-style execution. You create classes with methods that the engine executes at the appropriate time.

You create a Task class in cli scripts. Kinda looks like:

class Task {
    /**
     * handles the on load event
     */
    public static function eventOnLoad() {
        $args = print_r(F::$engineArgs, true);
        print($args);
    }
}

You create a Page class in www scripts. Kinda looks like:

class Page {
    /**
     * sends the submitted message
     */
    public static function actionSendMessage() {
        //validate
        if(F::$request->input("name") == "") {
            F::$errors->add("name", "required");
        }
        
        //take action
        if(F::$errors->count() == 0) {
            //send email
            F::$emailClient->addTo(F::$config->get("admin-email"));
            F::$emailClient->subject = "Contact Form Submission";
            F::$emailClient->message = "An awesome email message from: ". F::$request->input("name") .".";
            F::$emailClient->send();
        }
    }
}

You execute cli scripts on the command line through the cli/run bash script. For example:

//go to your project cli directory
# cd /myproject/cli

//run a script
# ./run import-feed/index.php 

The cli/run bash script is super basic. As you can see all we're doing is routing the task script's path to our cli/.controller.php script as an argument.

#!/bin/bash
php .controller.php $@

In cli/.controller.php we include the config first, then your task script and finally start the engine:

<?php

require_once(dirname(__FILE__) ."/../app/config.php");
require_once(dirname(__FILE__) ."/../cli/". $argv[1]);

/**
 * start the engine
 */
F::init();

From here the engine begins processing. Here is a summary of what happens in cli mode:

There's not much going on in cli mode compared to what www mode has going on, so we'll leave it at that.

Your www scripts get executed during web requests (*cough* Apache *cough*) so the first layer in the process is the www/.htaccess file.

<FilesMatch "(.xsl|.sql.xml|.php|.email.html)$">
    Order Allow,Deny
    Deny from all
</FilesMatch>
<Files ".controller.php">
    Allow from all
</Files>

RewriteEngine On
RewriteBase /

RewriteCond %{REQUEST_URI} !\.(php|zip|csv|css|txt|xml|js|gif|png|swf|ico|jpe?g)$
RewriteRule (.*)$ /.controller.php [L]

First we deny access to any senstive files we don't want exposed to web clients. Then we route stuff like media and data files normally through Apache. Any other requests get routed through the www/.controller.php file.

<?php

require_once(dirname(__FILE__) ."/../app/config.php");
require_once(dirname(__FILE__) ."/../lib/footprint/Router.php");

/**
 * first check the cache
 */
Router::cacheCheck();

/**
 * initialize the router
 */
Router::init();

So instead of just starting the engine, like a cli script, first we initalize the Footprint Router (lib/footprint/Router.php), which in turn starts the engine. The Router object helps us rewrite URLs and will serve cached files when appropriate. If cache is served, no resources are ever opened and the request is finished.

URL rewrites are handled with PHP. You control rewriting with .route.php files. These work similarly as .htacess files where the closest file wins.

Generally here is what the router does:

From here the engine begins processing. Here is a summary of what happens in www mode:

The engine namespace property (F::$engineNamespace) is like a path signature. For example, the engine namespace of http://myproject/login/index.html is login/index. It's the path without a file extension. This allows us to do some helpful things like auto include PHP/HTML/JS/CSS respectfully.

Take our home page for example (www/index.html). If we simply added the files www/index.css and/or www/index.js the engine will add the appropriate CSS or JS include tags into the document DOM before sending it to the client.

This HTML document:

<!DOCTYPE html>
<html>
    <head>
        <title>Home Page</title>
    </head>
    <body>
        ...
    </body>
</html>

Will automatically become:

<!DOCTYPE html>
<html>
    <head>
        <title>Home Page</title>
        <link href="index.css" rel="stylesheet" type="text/css">
        <script language="javascript" src="index.js" type="text/javascript"></script>
    </head>
    <body>
        ...
    </body>
</html>

You could obviously name your files something unique like www/cool.css and include the markup yourself manually. But it's nice to keep the set of files as a named unit and keep things unobtrusively seperated. The other advantage to the engine auto-includes is that you can also tell the engine to add unique query params like index.css?nocache=50392f1aa2ef2. This will force web clients to skip their cached copies.

You can include special META tags that the engine can use while processing. The engine automatically removes these tags before the final result is sent to web clients.

<meta name="require-session" />

This triggers the engine to execute the F::$user->requireSession() method. This happens after the Page::eventOnLoad() method and before the Page::eventBeforeActions() method.

<meta name="permission-id" content="840" />

This triggers the engine to execute the F::$user->continueOrDenyPermission(...) method. This happens after the Page::eventOnLoad() method and before the Page::eventBeforeActions() method. You should use the <meta name="require-session" /> META controller before this one to validate the user does indeed have a session before checking for permission.

<meta name="email-debug-log" />

This triggers the engine to email the system admin (F::$config->get("admin-email")) an email with the contents of the Engine's processing log. This is helpful when things don't go as planned.

<meta name="cache-rule" [day|hour|minute|second]="..." />

This triggers the engine to save the output of the request to a cached file in the www-cache/... directory. The expiration date for the conent is set for the duration supplied.

<meta name="force-unique-auto-includes" />

This triggers the engine to add unique query string parameters to auto-included files such as CSS and JS such as: <link href="index.css?nocache=50392f1aa2ef2" /> and <script src="index.js?nocache=74792f0zz4eg3"><script>.

<meta name="preload-sql" content="sql_id" [alias="..."] />

This triggers the engine to open the namespace .sql.xml file, find the SQL command using the supplied sql_id and store the results into the F::$customHashes[sql_id|alias] array property, ready to be used with HTML binders. You can name the array key by using the alias attribute, helpful for shorter HTML binder calls.

Bring data into your views (HTML files) without any PHP scripting in most cases. And when you do need to make scripted desicions, these binders are still ready work for you. After a binder has been processed the data-bind-x="..." attributes are removed from the document. We use div tags in these examples but binders will work on any element. If the data selected is null or not found no changes are made to the element. Here are the data-bind-x="..." attributes and what they do.

<div data-bind-text="selector">default value</div>

This will trigger the engine to find the data based on the supplied selector and set the inner text of the element to that value.

<div data-bind-html="selector">default value</div>

This will trigger the engine to find the data based on the supplied selector and set the inner HTML of the element to that value. Be Careful: you should sanitize any data you're bringing in from the outside world since this is being added to the DOM.

<div data-bind-attr="name=selector[,name=selector]">...</div>

This will trigger the engine to add attributes to your elements. You may add one name=value pair or comma seperate them to add multiple. The attribute will be the name you supply with the selector's value as the content of the attribute. If the value of an attribute's selector is null, that attribute will not be added to the element.

<input data-bind-input="selector" />

This will trigger the engine to handle input elements by setting values, marking list menu options as selected, handling checkbox and radio button values appropriatly. For <input type="text|password|hidden|submit|button|reset" /> elements we set the value attribute to the selector's data value. For <textarea></textarea> elements we set the inner HTML to the selector's data value. For <select></select> elements we find options with a value of the selector's data value and set the selected="selected" attribute for that option. For <input type="checkbox|radio" /> elements we set the checked="checked" attribute if the value matches the data selector's value.

<div data-bind-function="ClassName::methodName">...</div>

To use this binder you create a method in your Page class like Page::doSomethingCool($node, $data) or in a Helpers class like Helpers::sharedCoolness($node, $data). The cool thing is that when your function is called, you get the element passed as an argument for quick access. You'll need to experience this to see first hand just how neat it is. The $data argument passed to your function is neat too cause when you use it with data-bind-results the $data argument is from each item of the data set, giving you row by row conditional control. Notice how we didn't include the () parenthesis in the markup, that's on purpose, they should be ommitted.

<div data-bind-results="sql_id">
    <div data-label="blank-row">
        <a data-bind-text="selector" data-bind-attr="href=selector">default value</a> 
        <p data-bind-text="selector">default value</p>
    </div>
    <div data-label="no-results-row">
        <p>no results found</p>
    </div>
</div>

This triggers the engine to open the namespace .sql.xml file, find the SQL command by the supplied ID and loop over the results. For each row the engine will duplicate the element which has a data-label="blank-row" attribute, we call this a chunk. Then the engine binds that chunk with the row of data we're looping over. If there are results the engine removes the element with a data-label="no-results-row" attribute. If there are no results, the engine removes the element with data-label="blank-row" attribute. There are some other features of data-bind-results="..." binder such as paging and built-in column handling. It's also really cool to use data-bind-function="Page::rowHandler" on the data-label="blank-row" element, giving you row by row programatic control.

<div data-bind-rows="custom_hash_key">
    <div data-label="blank-row">
        <span data-bind-text="selector">default value</span> 
        <p data-bind-text="selector">default value</p>
    </div>
</div>

This works similarly to the data-bind-results="..." stuff above. However we don't get the data via SQL here and there isn't any special features included like built-in paging or the data=label="no-results-row" handling. The engine will look in the F::$customHashes[custom_hash_key] array property using the supplied custom_hash_key key. It then loops over the dataset and binds each chunk with the appropriate data. You'll want to fill up your custom hash data during the Page::eventBeforeDataBinding() method so the data is available when the engine needs it.

This is a great feature because it will handle your CRUD and be much more flexible. We're also integrated inline error messages with the same markup from Bootstrap. The following example is a CRUD screen, simply remove the data-bind-form-context="add-update-delete" attribute from the <form ...>...</form> element to turn CRUD handling off. Another great feature is that all this is post-back friendly.

This the data-bind-form="sql_id" binder will trigger the engine to load the name space .sql.xml file and find the SQL command by the sql_id supplied. It then stores the record data into the F::$formCache[sql_id] array property. The form's contents are then bound with this data, most importantly elements with the data-bind-input="..." attribute.

<form data-bind-form="get-record" data-bind-form-context="add-update-delete">
    <fieldset>
        <legend>User Information</legend>
        <div class="control-group">
            <label class="control-label" for="is_active">Is Active:</label>
            <div class="controls">
                <select data-bind-input="is_active" name="is_active">
                    <option value="yes">yes</option>
                    <option value="no">no</option>
                </select>
                <span class="help-inline"></span>
            </div>
        </div>
        <div class="control-group">
            <label class="control-label" for="username">Username:</label>
            <div class="controls">
                <input type="text" data-bind-input="username" maxlength="45" size="20" name="username" />
                <span class="help-inline"></span>
            </div>
        </div>
        <div class="control-group">
            <label class="control-label" for="email">Email:</label>
            <div class="controls">
                <input type="text" data-bind-input="email" maxlength="128" size="40" name="email" />
                <span class="help-inline"></span>
            </div>
        </div>
        <div class="control-group">
            <label class="control-label" for="password">Password:</label>
            <div class="controls">
                <input type="password" data-bind-input="password" maxlength="40" size="15" name="password" />
                <span class="help-inline"></span>
            </div>
        </div>
    </fieldset>
    <div class="form-actions">
        <input class="btn btn-primary" type="button" value="Create New" name="action" id="button_new" />
        <input class="btn btn-primary" type="button" value="Update" name="action" id="button_update" />
        <input class="btn btn-danger" type="button" value="Delete" name="action" id="button_delete" />
    </div>
</form>