Simple and flexible web apps using super fun technologies. Enhanced with with Bootstrap.
Just the basics: home, about, contact and login. Add and remove pages as needed.
Login system and reset password built in. There is also groups and permissions.
Setup with the basics and ready to be extended. Don't be shy, check it out.
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
mysql-db-structure.sql
file.app/config.php
file. Make sure you supply MySQL credentials.www/
.libxsl
module installed. We use version 1.1.26 compiled against libxml
version 2.7.8.root
. You should change that after install.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:
Task::eventOnLoad
methodF::$doc
(if used)Task::eventBeforeFinalize
methodTask::eventFinal
methodThere'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:
.route.php
file and loads contentsRoute::eventOnLoad
methodFrom here the engine begins processing. Here is a summary of what happens in www
mode:
helpers.php
files peer to the namespace pathPage::eventOnLoad
methodF::$doc
and processes meta instructionsPage::eventBeforeActions
methodaction
methodPage::eventBeforeBinding
methodPage::eventBeforeFinalize
methodPage::eventBeforeOutputGeneration
methodPage::eventFinal
methodThe 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>