PHP Framework for Rapid Application Development
After a couple of years of hacking this in my spare time, I feel that my API is stable enough for other people to play around with this code as well. It's released under the GPL, so feel free to use it, fork it, or do anything else that you want as long as you send me any improvements.
Paferalib is a set of components for developing web applications in PHP. It has a URL resolver, object cacher, database modeler, and all of the other convenience tools that you need in 2016. It's designed for use on any host that supports PHP 5, mod_rewrite, and MySQL or SQLite (although complex applications will really benefit from MySQL). It's lightweight enough to be useful on free or cheap hosting packages where bandwidth and/or disk space is limited, and is decently fast even on a Raspberry Pi. It's also bandwidth optimized for mobile devices which may not have the fastest Internet connection, and separates components for easy caching. With 50-80% of a typical website's visitors using phones or tablets instead of computers, small and efficient beats fat and slow anyday!
The philosophy behind Paferalib is that since modern devices have more than enough processing power and JavaScript engines have advanced dramatically since Internet Explorer was holding back the advanced web, we can achieve maximum efficiency by using the server to only process data and render the page on the device using JavaScript.
The workflow is thus:
This lets the page render almost immediately because the original page is nothing more than a skeleton, while the content is loaded as necessary as the user browses, providing the fastest possible viewing experience and keeping the server load to a minimal level. My experiences show that even a cheap phone with 1GiB of RAM with Chrome or Firefox installed is adequate for pages with complex scripting.
To use Paferalib to its full power, you should be comfortable writing your own SQL queries, sending data out through JSON to the browser, and then using JavaScript to render the data to the user. If you're such a person, then give this a try. I hope that it'll help you achieve your goals easily and quickly.
You can install paferalib simply by downloading the zip file and unzipping it into your web server's www directory. Afterwards, you can point your browser to your website and setup your database, scripts, and so forth using the administration interface.
The main structure is as follow:
.htaccess | Rewrites short URLs |
index.php | Loads the library files and resolves the URL |
apps | All application code and resources live here |
cache | Caches compiled code, pages, and database queries |
data | User-created images, videos, etc... |
libs | External libraries |
private | Unreadable directory from the web; useful for security logs |
paferalib | Main library directory |
Each application can contain the following directories
admin | Administration pages for the app |
api | APIs accessible via JSON |
css | Stylesheets |
js | JavaScript files |
models | Database models |
pages | Webpages |
plugins | Plugins to be run after the page content has been generated |
translations | Translations in JSON format |
Due to the rewrite rules in .htaccess, direct file access is limited. This is both for security and for convenience in using short URLs.
To shorten URLs and to save typing, resources for each application can be addressed using the following scheme:
apps/appname/pages/pagename | /appname/pagename |
apps/appname/css/filename | /c/appname/filename |
data/appname/filename | /d/appname/filename |
apps/appname/images/filename | /i/appname/filename |
apps/appname/js/filename | /j/appname/filename |
apps/appname/sounds/filename | /s/appname/filename |
apps/appname/videos/filename | /v/appname/filename |
Let's say you created an app called test, your main page would be at apps/test/pages/index.php and the URL would be /test/index or /test for short. I think most people would agree that the short form is much easier to type.
utils.php contains general functions which can be used by any code without any additional dependencies. As such, it's useful to look through this file before creating your own utility functions.
Converts SUIDs to short codes, which are six characters consisting of upper case letters, lower case letters, numbers, dash, and underscore. These characters were chosen because they are usable on almost all current filesystems.
The functions with s at the end work on arrays rather than individual values. This allows more than 32-bit numbers to be used.
Functions that work on arrays.
ChooseOne() randomly choses an value from the given array.
Average() returns the numerical average of all values.
Shuffle() returns an array with rearranged ordering.
KeyFromValue() returns the key belonging to the given value.
These functions are used only in the web environment.
Given a set of translations, this will search for the best available translation in the following order:
The default application for all root-level URLs is called h, which is short for home. To make our traditional "Hello, world!" page, simply edit apps/h/pages/index.php to read
<p>Hello, world!</p>
and your website's main page will read "Hello, world!"
All pages in Paferalib are PHP scripts. Let's say that you wanted an about page, you would create apps/h/pages/about.php. You could then update index.php to link to this page by using
<p>Hello, world!</p>
<p><a href="/about">About me</a></p>
Pages in Pafera are not in the global scope, but are actually inserted inside a function in resolver.php. This means that if you want to use any global variables, you need to write at the top of your page as in a function.
<?php
global $globalvar;
Another consequence of this scope is that the Resolver will automatically include several useful variables for you as well.
$pathargs | Any arguments passed via the URL but not by query string |
$D | The default database for the site |
$R | The resolver, which handles paths and files |
$S | The session object, which saves your variables into the database |
$T | The translator, which handles loading translations for various languages |
$T_SYSTEM | System translations for common tasks |
$T_MAIN | Translations for the site title, buttons, and other layout items |
The $pathargs variable contains any extra parameters to the script passed via the URL.
For example, if you have a script at the URL /view and the URL is /view/0/100/icon, $pathargs would become
['0', '100', 'icon']
This is the equivalent positional parameters of a function
function View($start, $limit, $style)
just using the URL rather than calling a function directly.
This is the default database for the site, which is initialized before it comes to your page. Paferalib supports using multiple databases simultaneously, which you may want to do if you're supporting multiple simultaneous users using SQLite to avoid locking down the whole database file down whenever a script wants to insert something. If you're using MySQL or anything else that locks only individual tables, it's unlikely that you'll use anything beyond this variable.
The Resolver is the object that you will use to identify which app you're in. It also has convenience functions for images, scripts, and other resources that your app might use.
For example, if your app is called test and you have an image at apps/test/images/logo.png, you can either type
<img src="/i/test/logo.png" />
or you can use
<?=$R->IMG('logo.png')?>
to do the same thing without hardcoding the name of your app into your code?
Paferalib supports app instances, which are copies of your app using different names implemented by Unix or NTFS soft links. Each app has the same code, but different data. If you want your billing department and your sales department to both have forums, you can use the same code under apps/billingforums and apps/salesforums to achieve this result.
It's also possible to have different code paths depending on the name of your app. For example, our file manager "share" doubles as a TV remote under the name "pitv." The additional features are activated by a code block that reads
if ($R->currentapp == "pitv")
{
...
}
The Resolver is also useful when including other scripts. Instead of writing
include('apps/test/libs/lib.php');
You can write
$R->IncludePHP('libs/lib.php');
to do the same thing or use
$R->IncludeDir('libs/lib1');
to include an entire directory of PHP files at the same time.
The session object exists to save session data into a database rather than on the filesystem. This allows many web servers to share a database server and thus have user sessions available anywhere. You should probably not worry too much about this unless you're a big company with multiple A records for your domain name.
Translations are natively built into Paferalib. On the application side, they're stored in JSON files in the translations directory and loadable by this object. On the database side, they're stored into a JSON field.
Let's say that you have an app called test with a translation file called main. You could then load these translations by calling
$T_TEST_MAIN = $T->Load('test/main');
print_r($T_TEST_MAIN);
$T_TEST_MAIN would then be an array of strings.
System-wide collection of strings useful on every page. Things like "Go," "Cancel," and "Back" to make your life easier.
Site-specific collection of strings. This should contain your site name and anything else that's useful on multiple pages.
The database class is... strangely enough... called DB. The default instance can be found in $D. All site settings including database login information can be found in the JSON file private/pafera.cfg. It would be a good idea to ensure that you don't accidentally send or upload this file anywhere.
Paferalib's database supports all of the normal SQL operations and includes object linking, translations, tagging, properties, and many other convenient tools. Of course, the downside to using any type of generic tools is that you trade development time for execution speed, but we like to stick by the old "Make it work first, then optimize later" philosophy.
The database class has several flags which you may find useful.
Signals to the system that this is a production system, which will make the following changes.
This will also make it much harder to debug your code, which is why it can easily be switched on and off as needed.
Paferalib does not hide its SQL backend. In fact, it puts it right in front of you and lets you write your own SQL queries to take advantage of your system.
Database models live in the models directory of your app. They take their name from the filename, are always lowercase, and support autoloading so that you don't have to include a file for every model that you want to use. Instead, the first time you create or search for a model is the time where it will be autoloaded.
A model file looks something like the following:
# file loginattempt.php
<?php
class templateclass extends ModelBase
{
public static $DESC = [
'numsuids' => 1,
'flags' => 0,
'uniqueids' => ['phonenumber', 'place'],
'fields' => [
'phonenumber' => ['TEXT NOT NULL'],
'place' => ['TEXT NOT NULL'],
'timestamp' => ['INT32 NOT NULL'],
'ipaddress' => ['INT32 NOT NULL'],
'flags' => ['INT32 NOT NULL'],
],
'indexes' => [
['INDEX', 'ids'],
],
];
}
The special "templateclass" keyword is the name of the model. This will vary depending on the name of the app. If your app is named "test," this model will become "test_loginattempt," and you will create it using the line
$attempt = $D->Create('test_loginattempt');
For convenience, the keyword "templateapp" will be replaced by the name of the current app. Thus if you have a sibling model called "user," you can load it inside the loginattempt definition using the code
$user = $D->Create('templateapp_user');
The preferred way to use models in Paferalib is to place the database definition and any commonly used functions in the model itself, but any logic that is used only once should be placed within an API script. This keeps down the amount of code which needs to be loaded and parsed every time an object is used.
The main definition is found in the static variable $DESC. This can have the following members:
The term "SUID" stands for Synchronization Unique ID, and is an implementation of a random ID across the int32 address space for every insertion. In simpler terms, every time you insert an object into the database, it automatically gets an unused ID from -2147483648 to 2147483647 excluding zero. It allows for Bob, Jack, and Mary to all have their own copies of the database, add their own items to it, and then come back and easily merge their changes into the main database, which would be rather inconvenient on a system which used automatically incrementing IDs.
This can take any value above zero, but remember that IDs take up space. 1 is a simple 32-bit value, but 4 takes up 128 bits for every row of your model. Unless you really need to store more than four billion rows, 1 should be enough for everyday use.
There are three ways to identify a given row in Paferalib
Unique IDs is an array of field names containing what makes this row unique. For example, the loginattempt model can be uniquely identified by the phone number and place of the user trying to login, since different places can have the same phone number.
It is quite possible for a model to have all three ways of identifying, in which case the database will first use the unique IDs, then try to use the SUIDs, then finally use the AUTO_INCREMENT ID.
Note that using unique IDs automatically creates an unique index for the fields in the database as well for efficiency.
The fields is the real definition of the SQL CREATE TABLE statement. It contains an array whose keys are the field names and the values are the field definitions. The definitions are an array in the form [type, validator, extra] with only the type required.
Available types are the common SQL types INT, FLOAT, TEXT, BLOB along with some custom Paferalib types:
This is an array which allows you to specify individual indexes to be created for the table. It takes the form [indextype, indexfields] where indexfields is a string containing field names separated by commas.
For tables which are frequently queried, indexes can dramatically improve performance, but for tables which are frequently written to or updated, indexes can dramatically lower performance. It's suggested that you test your tables both with indexes and without indexes to find the best fit.
You typically will not need to issue a CREATE TABLE statement yourself unless your table is really complicated. A model named test_user will result in a table called test_users being automatically created on first use.
To create an object, simply pass its name to the $D->Create() method. The class will be autoloaded, initialized with default values, and returned to you.
$obj = $D->Create($model);
You should not create new models using the PHP new keyword unless you manually initialize the fields yourself using $D->ImportFields(). If you have not setup correct validators, it's quite possible for bad data to be written into your database when you try to save your object.
Like most database abstraction layers, Paferalib has two ways to get an object: load it by ID or find it by criteria.
Loading an object can be done in many ways depending on the type of ID used.
# Load by SUID
$obj = $D->Load('test_loginattempt', $suid1);
# Load by named array
$obj = $D->Load('test_loginattempt', ['suid1' => $suid1]);
# Load by unique IDs
$obj = $D->Load(
'test_loginattempt',
[
'phonenumber' => $phonenumber,
'place' => $place,
]
);
All of these will produce a test_loginattempt object stored in $obj, throwing an exception if the current user does not have the view permission or if the object could not be found.
For efficiency, it is also possible to specify which fields to load and to provide an object to load into:
# Load only phonenumber
$obj = $D->Load('test_loginattempt', $suid1, 'phonenumber');
# Load into existing object
$D->Load('test_loginattempt', $suid1, '', $obj);
If you have a list of IDs, it is possible to load them all at once
# Loading from an array of IDs
$objs = $D->LoadMany('test_loginattempt', $ids);
but this is inefficient. Since Paferalib does not know in advance what type of search to perform for each row, it results in a query for every object. You should perform the load yourself for greatest performance using a WHERE clause and using $D->ImportFields() to create your objects.
Finding an object is probably about 99% of what most people use SQL for, and also can be the most complicated operation to do once you start doing JOINs and subselects and all of those fun features that DBAs specialize at.
Paferalib has portability and ease of use as two of its goals, so we do *not* include any database-specific functions. We use SQLite as a baseline, meaning that if it can be done in SQLite, it can probably be done in every other database as well. For most people, this won't make much of a difference, but if your app has performance-critical parts, you can always use a direct query with $D->Query() to optimize for your particular database. CREATE VIEW and the database's native query cache can also help for frequently used queries.
# Simplest form returns all rows
$objs = $D->Find('test_loginattempt')->All();
# Using a WHERE clause with parameters is the normal use
$objs = $D->Find(
'test_loginattempt',
'WHERE phonenumber = ?',
$phonenumber
)->All();
# ORDER BY can be written straight into the WHERE clause
$objs = $D->Find(
'test_loginattempt',
'WHERE phonenumber = ?
ORDER BY phonenumber',
$phonenumber
)->All();
# but LIMIT should be put into the options array due to chunking
$objs = $D->Find(
'test_loginattempt',
'WHERE phonenumber = ?
ORDER BY phonenumber',
$phonenumber,
[
'start' => 100,
'limit' => 100
]
)->All();
Like regular PDO queries return a SQL cursor, $D->Find() returns a DBResult class. The most commonly used style is to use a foreach loop to iterate over each row that $D->Find() returns, and DBResult is designed to support exactly such a use.
foreach ($D->Find(
'test_loginattempt',
'WHERE phonenumber = ?
ORDER BY phonenumber',
$phonenumber,
[
'start' => 100,
'limit' => 100
]
) as $r
)
{
print $r->phonenumber;
}
DBResult natively supports chunking, or reading a portion of rows at a time for processing to save memory. This means that if your database returns a million rows, DBResult will first fetch rows 0-999, then rows 1000-1999, and so forth. The chunk size defaults to 1000, and can be set using the 'chunksize' option.
DBResult also supports caching the returned objects to the webserver, thus vastly improving performance for subsequent queries with the same parameters. This can be enabled by setting the 'cachesize' option to the number of rows that you wish to be cached. The next time that you run the same query, DBResult will check to see if the number of rows in the table has changed. If the count is still the same, your objects will be loaded from disk rather than having to make another trip to the database server.
Like $D->Load(), $D->Find() supports retrieving only specific fields from the database to save processing time. You can set these as a string with each field separated by a comma in the 'fields' option.
It's also possible to retrieve only objects with certain permissions. For example, if you only wish to get objects which you have permission to change, you can set 'access' to DB::CAN_CHANGE and DBResult will return only those objects.
The top mistake to make with DBResult is that it will not return all of the rows of your database by default. Instead, it will only return the first 1000 rows, which saves a lot of processing for most common operations. If you wish to get rows past the first 1000, make sure to set the 'limit' option to a large number.
Because of security, chunking, and the processing limit, getting a precise count from DBResult when you have a secure model is only possible if you retrieve *all* rows and then manually count how many rows you have. For secure models, it's quite possible that certain rows are not viewable by your user and thus will be skipped over. Models without security do not have this issue since all of their rows are public.
All Paferalib models must inherit from ModelBase in order to receive variable tracking and validation capabilities.
ModelBase keeps track of which database this object came from and will forward most methods to that database. These methods are also normally chainable, so instead of doing
$obj = $D->Create('model');
$obj->Set(['property' => 'foo']);
$D->Insert($obj);
You could write the equivalent as
$D->Create('model')->Set(['property' => 'foo'])->Insert();
It's important that you remember that properties cannot be set directly as in
$model->property = 'foo';
but must be set using the ModelBase->Set() method
$model->Set(['property' => 'foo']);
While this will seem awkward, ModelBase->Set() will keep track of which variables have changed and which variables have not. When it's time to update the object, calling ModelBase->Save() will result in a no-op if nothing has changed.
An additional benefit is that if you have setup your validators correctly, searching for, loading, and updating an object from a POST request can be as simple as
$D->Create('model')->Set($_REQUEST)->Replace();
Most objects will require more processing than this, but for models which do not require advanced validation, this can be very convenient.
ModelBase has several useful methods which you might find yourself using from time to time.
One of the main frustrations with SQL is to decide what to do when you're trying to insert a row with the same ID as an existing row. The MySQL REPLACE keyword does a lot to help with this situation, but unfortunately is not supported by all databases, and thus not includable in Paferalib.
With all of these functions, the action of saving an object involves converting all of its properties into formats suitable for the database, calling any necessary validators, and then sending the command to the database itself. All of these can result in exceptions being thrown, so be sure to wrap any save operations in an exception handler.
Any special handling for a model can be done by defining the functions ModelBase->OnSave() for before the save is done and ModelBase->PostSave() for after the save is done. If you want to convert any special types or launch any hooks, this is the place to do it.
In its simplest form, $D->Delete() will truncate the model's table
$D->Delete($modelname)
It can also delete only specific rows
$D->Delete($modelname, 'WHERE phonenumber = ?', $phonenumber)
Plain and simple is the description for this method.
Paferalib natively supports linking objects to each other in a many-to-many relationship. These links can have a type, an order, and a comment as to what the purpose of the link is. It's not uncommon to see code like
$bob = $D->Find('user', 'WHERE username = ?', 'Bob')[0];
$tom = $D->Find('user', 'WHERE username = ?', 'Tom')[0];
$jane = $D->Find('user', 'WHERE username = ?', 'Jane')[0];
$mary = $D->Find('user', 'WHERE username = ?', 'Mary')[0];
$bob->Link($tom, BOSS);
$bob->Link([$jane, $mary], EMPLOYEES);
$bob->Linked('user', BOSS); # Returns $tom
$bob->Linked('user', EMPLOYEES); # Returns [$jane, $mary] in that order
Paferalib linking is unidirectional, so $bob -> $tom does not imply $tom -> $bob. In order to have both objects linking to each other, you need to link from both ends.
Linking can only be done by objects which are already saved. If you try to link an object that hasn't been saved to the database, an exception will be thrown.
Linking also only works within the same database. The links table does not keep track of which database an object came from, so do not try to link across databases. The results won't be what you expect!
Another limitation to linking is that your object must be uniquely identifiable, meaning that it must have an AUTO_INCREMENT ID, SUID, or unique IDs. If we cannot distinguish between objects, then we cannot return the right object.
These are the same as the SQL BEGIN, COMMIT, and ROLLBACK commands. You may nest these multiple times without problems, as Paferalib will keep track of how many layers you have created.
While Paferalib's default database security works wonderfully for limiting access via the JSON APIs, there will eventually come a time where you need to perform a task as an admin because your current user does not have the permissions to do what you need. $D->Sudo(), $D->Unsudo() will let you become an admin for a short period of time so that you can get things done and get back to work.
# Become admin
$D->Sudo();
[Do work as admin]...
# Go back to previous user
$D->Unsudo()
Database flags are normally set when the database object is first created, but sometimes you just want to debug a short piece of code. In that case, these convenience functions serve to temporarily enable or disable flags for the time.
# Enable debugging
$D->Debug();
[Code that needs to be debugged]...
# Disable debugging
$D->Debug(0);
Convenience functions for handling time. We recommend always using UTC to handle all of your times. paferalib/utils.php has GMTToLocal() and LocalToGMT() to convert between server time and local time using a JavaScript cookie set by the browser. Non-browser apps must convert to UTC before using any times in APIs.
$currentdate = $D->Date();
$currenttimestamp = $D->Timestamp();
sleep(5);
$thendate = $D->Date($currenttimestamp);
$thentimestamp = $D->Date($currentdate);
Convenience function to check exactly what type of access your current user has to this object. Returns a bitmap of permission flags such as DB::CAN_CHANGE and DB::CANNOT_DELETE.
For common use such as CHANGE or DELETE, it's easier just to use ModelBase->CanChange() and ModelBase->CanDelete() instead.
For database wide settings that don't depend on user, DB implements the ArrayAccess interface, which means that you can use it like a normal array with the database as a backend.
$D['script.timeout'] = 120;
echo $D['script.timeout'];
All values are kept in the h_config class, and are loaded as needed to avoid impacting normal code execution.
Convenience function to get the existing maximum ID for AUTO_INCREMENT ID models.
Paferalib supports native tagging of objects. The tags are separated by language, so you can display only the tags relevant to your current user.
For efficiency, only models with numsuids = 1 are supported at the moment. This can easily change in the future, but I haven't had a need to add support for any complicated models yet.
# Get an array of employees
$a = [
$D->Load('user', $bobid),
$D->Load('user', $tomid),
$D->Load('user', $janeid),
];
# Tag them as employees. Note that $language will default to
# $D->language unless otherwise set.
$D->Tag($a, 'employees');
# Further tag by gender
$D->Tag([$a[0], $a[1]], 'male');
$D->Tag($a[2], 'female');
# Get a list of all employees
$employees = $D->HasTag('user', 'employees');
# Get a list of all males
$males = $D->HasTag('user', 'male');
# Get a list of all tags for this model
$tags = $D->ListAllTags('user');
Paferalib also supports setting properties on objects. You can think of these as adding columns to your database table, but without the space needed on every row. These should be used when very few objects require these values, otherwise it's easier just to put another field into your model itself.
# Get an array of employees
$a = [
$D->Load('user', $bobid),
$D->Load('user', $tomid),
$D->Load('user', $janeid),
];
# Tag them as employees. Note that $language will default to
# $D->language unless otherwise set.
$D->SetProp($a[0], 'attitude', 'bossy');
foreach ($a as $u)
{
echo $u->username . ': ' . $D->GetProp($u, 'attitude') . "\n";
}
Although Paferalib is mostly original code on the PHP side since I couldn't find suitable code to do what I wanted, the JavaScript side includes many libraries by other people. As long as a library is small, light, and efficient, it's vastly easier to extend other people's work in JavaScript than it is to roll your own. If you compare the footprint of JQuery or JQuery Mobile to what we use here, you'll quickly realize the difference in space savings, which is quite important if you're on a phone using a 2G connection in rural areas.
As of right now, Paferalib uses the following libraries:
Libraries which are useful for multiple apps should be added to apps/h/js to make sharing easier for your site. I'm thinking of adding a dependency based automatic downloading administration page along the lines of the AMD loaders but implemented in PHP. However, it's only on the to-do list for now.
Libraries written/compiled by me include
Lets you interface with the Paferalib database straight from JavaScript.
Unfortunately, I haven't figured out a way to safely and securely handle this level of access for all users, so as of right now, only the objects administration page really utilizes this library.
If you enable the DB::PRODUCTION flags, all of these libraries get minified and compiled into one file at apps/h/js/all.js, meaning that it only takes one HTTP request to get all of them at once. As of right now, that file is 220KiB on my machine, whereas a full JQuery Mobile installation is easily two to three times the size. Of course, we don't have all of the functionality of JQuery + JQuery Mobile/JQuery UI either, but with 50-80% of current browsing done on phones and tablets, it's nice to keep things small and efficient.
To ease development, h_page automatically inserts several variables that you can use in your scripts.
Unlike the functions from paferapage.js, the functions in paferalib.js are in the global scope because they are useful in a variety of situations. Some of the more useful ones are as follows:
A shortcut for setting values in multidimensional arrays. This allows you to write code such as
a = NestObjects('foods', 'fruits', 'apples', 16);
console.log('You have ', a['foods']['fruits']['apples'], ' apples.');
without having to check the intermediary arrays.
One of JavaScript's major annoyances is its lack of multiline strings. Until I finish the Pafera Universal Language, this function will help by adding a list of strings as a single string. Compare the following for readability and amount of typing required:
// Adding a long string of HTML to a list
// String addition method
ls.push(
'<div class=Padded>'
+ 'This is a list',
+ '<ol>',
+ '<li>Item 1</li>'
+ '<li>Item 2</li>'
+ '<li>Item 3</li>'
+ '</div>'
);
// String list method
ls.addtext([
'<div class=Padded>',
'This is a list',
'<ol>',
'<li>Item 1</li>',
'<li>Item 2</li>',
'<li>Item 3</li>',
'</div>'
]);
If you ask why I don't use backslashes, it's because I like code to have proper spacing, and using backslashes completely ruins the spacing when you look at it in an editor.
And for those of you who want to use backticks (template strings), please check support for Android and IE and you'll start crying. 8-(
All properties and functions in paferapage.js live in the P namespace, so be sure to type P. in front of whatever you use.
Browser detection flags. Note that due to many browsers disguising their user-agent settings, more than one can be true.
P.useadvanced is a quick but convenient shortcut to figuring out whether you can enable advanced JavaScript functionality in your applications.
These all take a path and an app name as arguments and returns a URL pointing to the desired resource. If app is not given, then it defaults to the current app.
// Load main.css and main.js for the current app in parallel
_loader.Load([
P.CSSURL('main'),
P.JSURL('main')
]);
// Change an image
$('.ButtonIcon').set('@src', P.ImageURL('tux.png'));
// Play a sound
P.Play('buzzer', P.SoundURL('buzzer.mp3'), {autoplay: 1});
These two functions provide support for SVG sprites. The name parameter is a SVG view ID, while classes are additional classes to add to the image and size is the width and height in ems. P.IconURL() returns only the URL, while P.Icon() returns the full IMG tag.
// A call like
P.Icon('House', 'h', 'HouseIcon', '2.5');
// will return
<img src="/i/h/icons.svg#House" class="HouseIcon" width="2.5em" height="2.5em" />
To take advantage of this setup, your app should have a SVG file called icons.svg in its images directory and each icon should be defined by a view inside it such as
<view id="House" viewBox="0 0 100 100" />
Returns the element belonging to class containing the target element. For example, if you have
<div class=Card>
<div class=Title>Title</div>
</div>
Calling P.TargetClass(event, '.Card') when the title is clicked would give you the parent div.Card element.
Sets all elements to the largest width found. Useful for aligning floating elements in grids.
P.SameMaxHeight() does the same thing, except that it sets height rather than width.
The base function for all popups. Content should be a string or HTML content wrapped using the HTML() function. options include all Minified CSS attributes that should be applied to the popup itself plus some special fields:
P.ErrorPopup() is a shortcut to show an error dialog with bright yellow colors and a cute warning sign.
P.ClosePopup() will close the popup belonging to the specified class, or all popups if no class is given.
P.CloseThisPopup() will close the popup containing selector.
P.EditPopup() shows a popup for editing. This can be something as simple as a text control and as complex as an entire tax form. The custom field option gives a variety of options for formatting and extended functionality such as selecting and uploading images.
fields is an array containing fields for editing. Each field is an array of up to six elements containing the following:
On small screens, P.EditPopup() will render in fullscreen mode. On other screens, it will show up as a normal popup.
Being a complicated function, P.EditPopup() has many options.
onsuccess is a function receiving the form class, values, and the bottom results display.
// A simple dialog to get the user's name
P.EditPopup(
[
['username', 'text', '', "What's your name?"]
],
function(formclass, values, resultsdiv)
{
resultsdiv.fill('Your name is ' + values.username);
}
);
This function is responsible for the responsive button bars that animate on click used extensively on pafera.com. It takes a selector and renders the elements described by buttons into it.
buttons is an array containing button descriptions, which can consist of
[displaytext, onclickfunc, color, buttonclasses]
In the special case that onclickfunc is the string "custom," displaytext should be a HTML string containing the control to render at the current position.
color is a value from 1 to 6 depicting the normal Paferalib button colors, which are red, orange, green, blue, purple, and brown. Leave this blank to automatically assign colors. Yellow is reserved for hover elements.
buttonclasses are any extra classes applied to this button. Each button will already have the class "Button{num}" where num is the index of the button within the bar starting from 0.
A floating button is a special button which is fixed within the viewport and can be dragged around to a user's preferred spot. The idea came from Android's round action button which normally reside to the lower right corner of an app and makes clear what the current default action is. On pafera.com, we use it to implement the quickfuncs button, which is basically a command-line for the website.
Depending on what your site is, you may find it useful, or you may disgard it entirely.
Enables sorting and deleting functionality for the table found by selector. The helper function P.SortableTDs() returns HTML for three tds for moving the current row up, moving the current row down, and deleting the current row. These should be the last three columns in your table for all rows. onchangefunc, if provided, is called whenever a row has been changed.
This function is designed to easily shuffle rows on small screens such as phones. For computers with the greater control of a mouse, you should use drag and drop instead.
Creates a series of accordions like Wikipedia mobile uses where a quick click can collapse or expand the contents of this element. To use this correctly, each section should have a <div> surrounding it such as:
<h1>Title</h1>
<div>
<p>Content</p>
<p>Content</p>
<p>Content</p>
</div>
<h1>Title</h1>
<div>
<p>Content</p>
<p>Content</p>
<p>Content</p>
</div>
<script>
P.MakeAccordion('h1');
</script>
Does the same thing as SortArray(), only with the DOM tree.
Let's say that you have the following:
<ul>
<li>oranges</li>
<li>apples</li>
<li>grapes</li>
</ul>
Calling P.SortChildrenByText('ul') will result in
<ul>
<li>apples</li>
<li>grapes</li>
<li>oranges</li>
</ul>
children1 and children2 are selectors which give you fine control over exactly which child elements are used for sorting in nested lists.
Toggle buttons are the Paferalib equivalent of checkboxes. They're special buttons which have no rounded corners and change colors to indicate their condition. Gray means no action, green means on, and red means off. Normal toggle buttons only switch back and forth betwen gray and green, while tritoggle buttons add the red option. To set these, use the data-toggled attribute with "unset," "on," and "off" values.
P.ToggledButtons() will return an array of all toggled buttons within the selector. The key will be data-name and the value will be the toggled value.
Whenever a toggle button changes its value, it will check the data-ontoggle attribute. If it's set, it will call the attribute as a global function with the parameters (event, element, newstate). This can be convenient if you're using toggle buttons to redisplay the page.
Radio buttons are either a series of buttons placed horizontally or a select element if the buttons are too many to comfortably fit within a line. The default behavior is to switch to a select if there are more than eight buttons.
buttons is an array containing [displaytext, value, initialstate, buttonclasses]. If a select is used, then buttonclasses won't be applied to the option element.
Sets variables in the current user's session. Note that due to security concerns, a user's session variables are not available via JavaScript. You must include a section in your page such as
// Sets variables
P.Set({
'maps.zoomlevel': 5,
'maps.usegps': 1
});
// In your PHP page
<script>
maps = {};
maps.zoomlevel = <?=$_SESSION['maps.zoomlevel']?>;
maps.usegps = <?=$_SESSION['maps.usegps']?>;
</script>
All of these functions call Paferalib APIs by sending data to a script and reading the JSON returned. The difference is that P.API() does nothing special while P.LoadingAPI() will fill resultsdiv with a loading GIF until the call returns and P.DialogAPI() will popup a loading GIF in the middle of the page. If the API returns JSON data with a property named error, it will be shown in resultsdiv.
options is the same as for P.AJAX() since that is the underlying call.
onsuccess will be given the JSON data returned already parsed. P.LoadingAPI() will have an extra parameter named resultsdiv to make it easier to change the loading GIF into a success message.
Shows a loading GIF and loads the requested page.
If contentonly is set, then loads the contents into options.contentdiv, which defaults to #Content. This, in effect, is a JavaScript refresh of the page without touching the header, navbar, or footer. On the server side, h_page will notice the query string contentonly=1 and will skip rending the full page, saving a decent bit of processing.
options can include the following:
This function creates a buttonbar showing how many pages are available to the user and controls for selecting which page to go to.
count, start, and limit should be obvious. numonpage is how many items are on the current page. listfunc is the name of the global function to call to change the page, and defaults to "ListObjects."
listfunc should accept two arguments: start, and limit.
Plays media. Currently, this only plays audio files using howler.js as the backend.
options has only the property "autoplay" at the moment. If it's not set, the sound is simply cached for instant play at a later time.
These two functions animate a fullscreen window which slides in from the left. There are four layers by default: blank, 1, 2, and 3. This allows a fullscreen window to have yet another fullscreen window on top. The requested window can be set by the num parameter. classes are extra classes to add to the window, while contentlist is a list of strings for the window's content.
Fullscreen windows are handy for things like panels, playlists, and other such UI elements that take up a good portion of room but don't require a page reload.
These functions are similar to P.EnterFullScreen() and P.ExitFullScreen(), except that they show drawers instead. Drawers is a window which slides out from an edge much like the Windows taskbar or iOS title bars, and is handy for quickly accessing information regardless of where the page is scrolled.
side can be "Left," "Top," "Right," or "Bottom." If not set, it defaults to "Bottom."
size is how far the bar sticks out from the edge, and defaults to 2em.
classes are any additional CSS classes to apply to the drawer.
contentlist is a list of strings containing the HTML for the drawer content
Creates a calendar month inside selector. month should be in the format "2016-05" or blank for the current month.
If switchmonthfunc is set, the user will be able to choose the month by clicking on the buttons next to the current month. The function will be called with the selector and the desired month as parameters.
If dayclickfunc is set, whenever the user clicks upon a day, this function will be called with the event, the day element, and the date of the day.
The quickfuncs button is a floating button that appears on every page and allows functionality to be quickly accessed via a command line. It's primarily for use on mobile, but desktop users who like to use the command line should also find it handy as well.
Paferalib will activate this button by default whenever a user has logged into the site. It only has four initial functions: change password, choose bot, logout, and exit. However, any page can add commands using the P.AddQuickFunc() function.
To use a quickfunc, simply click on the button, type in your command, and then press alt+enter. If you're used to typing cd and ls, you shouldn't have any problems using it.
P.AddQuickFunc() takes four arguments: the base command, the command with all available actions, the description of what the command does, and the JavaScript function to call. Upon the user typing a command in, Paferalib will search for the base command from longest string to shortest string, then send the text behind the command and the resultsdiv to the function. Your function should do its work and then place a success message inside resultsdiv to finish.