PHPPE is a minimalistic, yet feature-full micro-framework and CMS. The framework's core is a single file and only a few kilobytes in size, so small, that it fits on your clipboard! Unlike other existing OpenSource PHP frameworks, PHPPE was written with security, MVC, KISS principle and no dependency at all in mind. As being a micro-framework, it won't solve all of your web-development oriented tasks, but will definitely solve the most common ones and make your life easier. It's not bloated, and with simplicity cames stability and high performance.
As it contains 7 bit ASCII characters only it's safe to paste over terminals for easy installation. As a matter of fact this single html file you're reading is enough to set up a working PHPPE environment, internet connection is NOT required at all!
This ~90k bytes of PHP code will give you:
Of course one single file is limited, so here's the PHPPE Pack (another ~90KiB) to save the day and give you an easy start with configuration registry, email services, user management, SQL Query Builder etc.
For full CMS capability you'll also need the Content Editor in PHPPE CMS (46KiB), because PHPPE Core on it's own only serves contents.
PHPPE Core, PHPPE Pack, PHPPE CMS, PHPPE Extensions as well as PHPPE Developer are free and OpenSource softwares, licensed under LGPL-3.0+. See LICENSE for details.
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
Although the framework is free, keep in mind that extensions may or may not use different licenses. PHPPE's licensing allows you to implement a closed source application on top of it.
Files | Library | Templater |
---|---|---|
|
|
|
What I mean by that is a really-really-really small foot print, not in a CI (11MiB) or lumen (30MiB) sense, but measured in couple hundreds of kilobytes. I wanted to run this framework efficiently on micro-computers, later my choice fall on Raspberry Pi.
Must work on it's own, but has to be able to detect and corrigate it's environment by building up directory structures if necessary without any kind of help.
Only optional dependencies. The main reason why other frameworks fail being small and fast lies in their huge amount of dependencies.
There's no reason to include a bloated library if only one or two methods is used out of it. PHP has a reach, constantly expanding function set, there's always a better, simpler way with pure php functions. You can argue with that, but time has shown that I'm right.
Also there should be about a dozen, clear event hooks. Too few or too many hook-points reflects serious design flaw.
It has to be a Model/View/Controller framework, meaning you can build custom webapplications on top of it if you want easily. For that it must provide all key features of a modern PHP framework (ORM, class autoloading, templater, etc.)
It was also important that if no webapplication found for an url, it must serve contents from a database or cache.
This is the main reason of low foot print and why caching is so sophisticated in PHPPE. Not to mention nginx compatibility and the built-in benchmarking feature.
PHPPE was designed in a way that it can run on several load-balanced, dynamically started/stopped content provider servers, all feeded by a single content editor server.
It's a very important design goal that installing an Extension should not require anything else than copying files under vendor/phppe/. If file access rights are correct, no need for running diagnostics mode.
As a part of the previous goal, it has to be able to install new tables transparently, without of the need of loading sqldumps with third-party utilities.
Security is not something that you can achieve by constantly patching your codebase. It has to be designed that way, making strict decisions, like:
The Core must be stable and well-tested. No new features or interface changes within major php releases allowed there, that's what well-though hook points and Extensions are for.
Eliminate the need of PHP coding as much as possible for webpage changes. This means that every aspect of the page should be stored as data, so that it can be changed from a web interface without coding. As for the Models, no field specific stuff should be hardcoded in controllers, so changing the database scheme won't lead to chaging the code, only Model class top.
By doing so makes the website changes easy, rapid and cheap as no development involved.
From an administrator's point of view, it must have a short learning-curve and intuitive interface. It's good if that also can be achieved at developer level, but not so important. An application is written once, but used many-many times, that should make the priority clear.
PHPPE has a very comfortable build-up. The PHPPE Core, the application as well as all extensions share the same directory structure.
This is the working directory of PHPPE. When you specify any path within PHPPE, you'll have to use paths relative to this directory. This saves you a lot of typing in your code and ease the migration across servers.
.tmp/ | directory where the webserver temporarily saves uploaded files, caches etc. |
app/ | your application, follows PHPPE Extension structure. In next chapter replace "vendor/phppe/extension" with "app" for application. |
data/ | permanent, local file storage (subdirectories are unspecified, as you like, but using separate subdirectory for each model and application is strongly advised) |
phppe/ | symlink to vendor/phppe/Core for convience |
public/ | used by the webserver, index.php and static files goes here. In your webserver's configuration when you set up DocumentRoot, you should point here: "ProjectRoot/public". |
vendor/ | main code directory. All directory beneath vendor/phppe shares the same PHPPE Extension structure |
composer.json | PHP Composer metadata of your project |
These directories are optional. If an extension does not provide let's say controllers, ctrl/ can be ommited.
vendor/phppe/extension/composer.json | Extension packaging information for PHP Composer and PHPPE Extensions. | |
config.php | configuration file. | |
init.php | Extension initialization file. These will be included automatically for every page |
libs/classname.php | model libraries, included by applications or other libraries. Also business logic goes here. | |
sql/table(.engine).sql | database schemes for tables used by models. It's highly recommended to prefix table names with lowercase extension name, like users, users_history, forum_topics, forum_posts etc. You can also use engine specific extensions, like ".sqlite3.sql". |
ctrl/action.php ctrl/application_action.php | page handler logic goes here. First holds code for common code, second is for action specific code. | |
libs/extension.php | If you don't want to separate controller files, you can but action methods in your service class here. |
views/application_action.tpl views/application.tpl | view templates. If action specific template found, it will be used. | |
addons/addon.php | if the extension provides Add-Ons for the view layer. | |
css/ | extension specific stylesheets | |
images/ | images used by the extension | |
js/ | javascript libraries provided by the extension | |
fonts/ | webfonts provided by the extension | |
lang/lang.php | language specific text definitons for each extension | |
out/format_header.php out/format_footer.php | for formats other than HTML (like RSS), their header and footer generating code goes there |
phppe/config.php | the framework's global configuration |
data/log/extension.log | logs are generated here. The files php.log, phppe.log, db.log, diag.log and validate.log are handled by the Core iself |
public/.htaccess | permissions and url rewrite directives (used by Apache webserver. Not needed with nginx) |
public/favicon.ico | your project's logo |
public/index.php | the framework itself (don't touch) |
app/config.php | your project's configuration |
app/init.php | your project's initialization code, for example url routing goes here |
app/css/ | your project's stylesheets. One for each theme |
app/images/ | images used by your project's face (no content specific imagery allowed here) |
app/js/ | javascript libraries required by your project |
vendor/bin/ | if you need shell or other non-php scripts (like a special nagios plugin) for your project, it should be placed here. |
vendor/phppe/*/libs/ds_engine.php | SQL translations and connection initialization commands for datasources. |
It's very important that your working directory is ProjectRoot. Therefore all symlink paths should be relative in PHPPE.
File | Link to | Purpose |
---|---|---|
phppe | vendor/phppe/Core | This link is merely a shortcut in ProjectRoot. |
vendor/phppe/app | app | This link points in opposite direction on purpose: we don't want composer to accidently overwrite your project's files. Also, app/ is managed by your git repository, whereas extensions in vendor/phppe/* is stored on github. |
app/css | public/css | Your application's assets. The symlink points to a subdirectory in "public/", which means they will be served by the webserver as static files without php interpretation. This does not mean you cannot use dynamic assets in "app/", Core will handle them just like any other extension's assets. |
app/js | public/js | |
app/images | public/images |
<!var>
, <!field>
, <!widget>
and <!cms>
tags. They have to be inherited from PHPPE\AddOn.
<!app>
tag to mark the place of your application, in other words this tag separates frame header and footer. Header usually contains a page menu and a navigation bar, while footer has a sitemap and an impressum line.
'maintenance'=>true
. In this mode controllers are skipped and only a view displayed using a template called "maintenance". This is
used to temporarily put your site down until you finish with delicate upgrades.
'runlevel'=>X
. This modifies how PHPPE behaves and the verbose level of logs it generates.All-in-one commands for people who are too lazy to read through:
composer create-project bztsrc/phppe3 mv phppe3 (yourproject) && cd (yourproject) $EDITOR phppe/config.php
git clone --branch 3.0 https://github.com/bztsrc/phppe3.git mv phppe3 (yourproject) && cd (yourproject) sudo php public/index.php --diag $EDITOR phppe/config.php
mkdir public curl https://raw.githubusercontent.com/bztsrc/phppe3/master/public/index.php >public/index.php sudo php public/index.php --diag composer update $EDITOR phppe/config.php
mkdir public cat >public/index.php copy this, paste here and press ^D sudo php public/index.php --diag $EDITOR phppe/config.php
Open up a terminal, and ssh to your server (or use a windowed ssh client). Change working directory to your ProjectRoot, then type
$ mkdir public $ cat >public/index.php
Copy the Core to your clipboard: click here, select all and copy. Switch back to Terminal, paste the code from clipboard then press Ctrl+D.
If your terminal is laggy and you cannot paste the code without mistakes, copy'n'paste this much shorter command that downloads the file from the server.
From server$ curl https://raw.githubusercontent.com/bztsrc/phppe3/master/public/index.php >public/index.php
If you take a look at your site in a browser, you'll see a greeting message:
Now framework is in place, it's time to run diagnostics to create directory structure and missing files. Without the "sudo", file ownerships and permissions may be left incorrect.
$ sudo php public/index.php --diag
If you failed to copy'n'paste the code correctly, you'll see this:
PHPPE-C: Corrupted index.php LOG-C: unable to write data/log/phppe.log
Root privilege is only needed for setting up file permissions, if you run it as a normal user, chown and chgrp may fail. In this case you'll see a warning:
DIAG-W: not root or no php-posix, chown/chgrp may fail!
If your webserver's group cannot be autodetected, or you run your vhosts under different users with setuid and setgid, you can specify the desired group id like:
$ sudo php public/index.php --diag --gid=33
If everything went well, you should see this message:
DIAG-I: OK
Check your site in a browser. You should see something similar to this:
_REQUEST: array (size=0) empty _FILES: array (size=0) empty | core.req2arr('obj'): array (size=6) 'f0' => '' (length=0) 'f1' => '' (length=0) 'f2' => null 'f3' => '' (length=0) 'f4' => false 'f5' => array (size=0) empty |
The --self-update option will update the Core to the latest version and it will also install PHPPE Pack (see step 5) and PHPPE Extensions Manager (see step 6). It's useful if you don't have Composer or you prefer web interfaces and want to minimize your command line activity.
$ nano phppe/config.php $ vim phppe/config.phpIt's very likely that you have an $EDITOR environment variable holding default editor's name.
You can use the passwd utility to generate masterpassword hashes from command line.
$ php public/index.php passwd Password? You won't see what you type here $2y$12$VUzOd391c2oYiESz3FSi2eZz1Gk5B9A2Kz66rvuNkkR04xQ/6yp0G
The diagnostics mode created missing files, among others composer.json. With that you can make PHP Composer to install vendor/phppe/Core from PHPPE Pack:
With Composer (not using Packagist)$ composer updateWithout Composer
$ curl https://bztsrc.github.io/phppe3/phppe3_core.tgz | tar -xz -C vendor/phppe/Core && sudo php public/index.php --diag
$ composer require phppe/ExtensionsWithout Composer
$ mkdir vendor/phppe/Extensions && curl https://bztsrc.github.io/phppe3/phppe3_extensions.tgz | tar -xz -C vendor/phppe/ExtensionsYou'll find it's configuration in vendor/phppe/Extensions/config.php. Once you've set up this, you can configure all the other Extensions over a nice web interface. This is the recommended way.
$ composer require phppe/CMSWithout Composer
$ mkdir vendor/phppe/CMS && curl https://bztsrc.github.io/phppe3/phppe3_cms.tgz | tar -xz -C vendor/phppe/CMS
This extension is not installed automatically as in a cluster you only need to install CMS on the management node(s).
PHPPE has a concept of an url-related application that has several actions, each operating on a specific item. An application houses one or more constroller classes, in which every action is mapped to a method, that will receive item as argument.
If no URL Routing rules applies to the queried url, it will fallback to default routing, which parses the url as
(base) / application / action / itemhttps://my.domain/some/path/users/modify/joesmithwhich will call
modify("joesmith")
method of PHPPE\Ctrl\Users
class.
See routing section of this documentation if you want more sophisticated control on which method with which arguments to be called for a specific url.
php public/index.php users sendnotifymails php public/index.php users delete joesmith php public/index.php cron dailytasks php public/index.php --version php public/index.php --help php public/index.php --diag
It's worth setting up an alias for PHPPE
alias phppe = 'php public/index.php'
1
include("public/index.php");This will bootstrap the basic PHPPE Core, autoloads classes and initializes services, but does nothing more. If you also want to load an application, run a controller in it and generate a view for it, you'll also need
1
2
$phppe = include("public/index.php"); $phppe->run();
PHPPE Core is configured in phppe/config.php, with an array. Available properties can be found on PHPPE\Core Class documentation, these are only the most commonly used ones.
db | specifies primary database connection |
title | name of the site (page title) |
runlevel | select production (0), verbose/testing (1), developer (2) or debug (3) mode |
maintenance | if set to true, will bypass all routing and controller, and only displays mainteance view for all urls |
mailer | Email backend, used by PHPPE Pack's Email sender, like "smtp://[(user)[:(pass)@]](server)[:(port)][/(sender email address)]" |
allowed | list of allowed functions in an array, leave it unset for no restrictions. |
disabled | list of disabled Extensions |
blacklist | list of unwanted domains (for example in email addresses) |
masterpasswd | Encrypted password hash for superadmin (see passwd utility to generate hash) |
repos | array of third-party repository urls. For details, see Extensions repository section. |
tz | Override autodetected client timezone (for embedded systems) |
... | See PHPPE\Core for all properties |
Your application's configuration resides in app/config.php. Your application's init() method will receive it as it's only argument.
Every extension has it's own configuration file, named vendor/phppe/*/config.php. When initializing, their init() method will get the configuration array.
Configuration files for Extensions (and for Application as well) required to return associative arrays:
1
2
3
4
5
<?php return [ "key" => "value", //...more properties ];
Unlike any other OpenSource PHP framework, being secure is one of the main concerns of PHPPE. It uses different methods at several levels, which all together provide a decent security for your application.
With it's built-in diagnostic feature, PHPPE will check these (and if run with root privilege, also fix issues on it's own).
The file owner can belong to webserver's group, but it strictly forbidden to set webserver as owner.
</body>
element.
Here's a regexp to grab the info from it.
<!-- MONITORING: ([A-Z]+), page ([0-9\.]+) sec, db ([0-9\.]+) sec, server ([0-9\.]+) sec, mem ([0-9\.]+) mb[, mc] --> </body>
MONITORING | magic to match pattern OK - everything was ok WARNING - running was ok, but generates more log than necessary ERROR - something (like a validator or security check) failed |
page | total page generation time including database time |
db | time consumed by data source queries |
server | webserver time (framework start time minus http received time) |
mem | maximum memory peak |
mc | flag exists if the page was served from cache. |
(vhost name):PHPPE-(A|W|E|C|I|D)-(extension):(original message without line breaks)It's easy to grep.
1
2
3
4
//send all messages to syslog, don't write log files. 'syslog' => true, //also log debug trace along with messages 'trace' = true,
$ crontab -e
* * * * * /usr/bin/php (your DocumentRoot)/index.php --diag
* * * * * /usr/bin/php (your DocumentRoot)/index.php cron minute
*/15 * * * * /usr/bin/php (your DocumentRoot)/index.php cron quaterly
0 * * * * /usr/bin/php (your DocumentRoot)/index.php cron hourly
5 0 * * * /usr/bin/php (your DocumentRoot)/index.php cron daily
*/20 * * * * /usr/bin/php (your DocumentRoot)/index.php cron myjob1
*/21 * * * * /usr/bin/php (your DocumentRoot)/index.php cron myjob2
It's important that in this case the script will be running with owner's rights. Or, if you're a system administrator, you can use system wide configuration file and specify user
/etc/cron.d/phppe_(vhost name):* * * * * (vhost's user name) /usr/bin/php (vhost's DocumentRoot)/index.php cron minute */15 * * * * (vhost's user name) /usr/bin/php (vhost's DocumentRoot)/index.php cron quaterly 0 * * * * (vhost's user name) /usr/bin/php (vhost's DocumentRoot)/index.php cron hourly 5 0 * * * (vhost's user name) /usr/bin/php (vhost's DocumentRoot)/index.php cron daily
Example action handlers for these commands can be found in events section. For each job, you'll have to implement cron + ucfirst(jobname) ($arg)
, like cronMinute($arg)
or cronMyjob1($arg)
.
You can create background jobs programatically as well with Tools. With this interface the scripts will run with the webserver's rights.
1
2
//! call $myClass->myMethod('cleanup','me') every 5 secs. \PHPPE\Tools::bg( "myClass", "myMethod", [ "cleanup", "me" ], 5 /*secs*/ );
PHPPE is tested under Apache and nginx webservers. Apache is more common, while nginx has better performance. Both are capable of url rewriting which is a key feature to hide 'index.php' from the url. Here are two examples on how to do it.
<VirtualHost *:80> ServerName mysite.tld DocumentRoot "/var/www/mysite/public" <Directory "/var/www/mysite/public"> Options -Indexes +SymLinksIfOwnerMatch +ExecCGI AllowOverride None Order allow,deny Allow from all RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ index.php/$1 </Directory> CustomLog /var/log/apache2/mysite-access.log combined ErrorLog /var/log/apache2/mysite-error.log php_admin_flag register_globals off php_admin_flag allow_url_fopen off php_admin_flag allow_url_include off php_admin_value open_basedir /var/www/mysite php_admin_value upload_tmp_dir /var/www/mysite/.tmp </VirtualHost>
user www-data; server { listen 80 default_server; root /var/www/mysite/public; server_name mysite; location / { try_files $uri $uri/ =404; index index.php; if (!-e $request_filename){ rewrite ^(.*)$ /index.php; } } location ~ \.php$ { fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param REQUEST_URI $request_uri; fastcgi_param DOCUMENT_URI $document_uri; fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param HTTPS $https if_not_empty; fastcgi_param GATEWAY_INTERFACE CGI/1.1; fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param REDIRECT_STATUS 200; ## !!! important !!! use $host for catch-all fastcgi_param SERVER_NAME $host; fastcgi_pass unix:/var/run/php-fpm.sock; } }
The framework has two different running scheme. PHPPE\Core constructor controls which one of these should be called. The $islib boolean variable is set if the framework was included from another php file as a library.
constructor ↓ autoload ↓ (CLI and has "--diag"?) y ↙ ↘ n bootdiag() Init event ↓ ↓ Diag event ($islib?) ↘ n run()
The following files will be processed in order:
public/index.php | called by webserver directly or via rewrite mechanism |
vendor/phppe/Core/config.php | framework configuration |
vendor/autoload.php | if found, PHP Composer autoloader will be included |
vendor/phppe/*/init.php | init scripts for extensions |
vendor/phppe/*/config.php | configurations for extensions |
vendor/phppe/*/lang/? | language dictionaries for extensions |
vendor/phppe/*/libs/? | other libraries referenced by extension's init.php |
The URL routing mechanism chooses one controller class in an application to run. To do that, PHPPE\Core::run() consult the ClassMap to find the class.
Finally templater will be called to generate output for the specified view. For CLI scripts templater is skipped unless you explicitly set a template in the action handler.
Because templater will try to find a view on it's own, it's possible to omit controller for a page entirely. This is very useful for pages like "About Us".
vendor/phppe/*/out/format_header.php | note that 'html' format has a built-in fallback. This code is responsible for generating the html , head blocks and body tag. |
view for 'template' | First, the template given in PHPPE\Core::$core->template will be looked up in these places. If that fails, application_action will be used as template name. If even that fails, as a last resolt application's name will be looked for. If neither found, templater will fall back to the 404 template. |
view for 'frame' | After application's output is generated and PHPPE\Core::$core->noframe is not set, output will be wrapped in this frame template. This is the template that holds page header (some nice, wide image usually), the navigaton bar and the page footer (with copyright notices and sitemap, Impressum etc. links). |
vendor/phppe/*/out/format_footer.php | finally footer is generated. Built-in version for 'html' includes JavaScript libraries here, and calculates values for monitoring comment before the /body tag. |
First of all, if PHPPE\Core::$core->maintenance is true, all routing will be bypassed and only maintenance view will be displayed (using maintenance.tpl template). Otherwise the routing decision is made in PHPPE\Core::run().
PHPPE has a concept of application that has several actions, each operating on a specific item(s). With other words, application is a controller class, action is a method in it, and item is it's argument (or more items separated by slash '/'). To explain URL routing, first we'll need an example application with actions. PHPPE gives you the freedom on how to organize your code. You can put all of your application's actions in a single file, or you can create separate files for each action. That's your choice.
app/ctrl/myapp.php1
2
3
4
5
6
7
8
9
namespace PHPPE\Ctrl; class MyController extends MyApp { //this action will be called via url routing with specific arguments function myspecaction( $arg1, $arg2 = "" ) {} //without route() call, arguments will be splitted by directory separator function myaction( $item, $item2 = "" ) {} //default action function action( $item ) {} }app/ctrl/myapp_myaction.php
1
2
3
4
5
namespace PHPPE\Ctrl; class MyAppMyAction extends MyApp { //if you use separate files, only one action handler defined in each. function action( $item ) {} }
Routes are loaded with PHPPE\Http::route() in one of the extensions (or your applicaiton) initialization file, and they will be matched against the url. If url mask does not contain pharenthesis (no arguments), the remaining of the url will splitted at slashes and passed to the method as argument(s). If method is omited, default action route will choose the appropriate method.
Routes has to be placed in app/init.php or vendor/phppe/*/init.php as extension initialization code gets called before the routing decision is made. It's too late to call PHPPE\Core::route() in a controller's constructor.
You can pass any class that can be autoloaded by PHP Composer to PHPPE\Core::route(), meaning the controller can be anywhere under vendor/ directory.
app/init.php1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//plain arguments PHPPE\Http::route( "myspecurl/([^/]+)/?(.*)", "MyApp", "myspecaction", "post,@admin" ); //json friendly format PHPPE\Http::route( [ "url" => "myspecurl/([^/]+)/?(.*)", "name" => "MyApp", "action" => "myspecaction", "filters" => [ "post", "@admin" ) ] ); //compact format for adding multiple routes at once PHPPE\Http::route( [ [ "myspecurl/([^/]+)/?(.*)", "MyApp", "myspecaction", ["post", "@admin"] ], [ "myspecurl2/([^/]+)/?(.*)", "MyApp", "myaction", ["loggedin"] ], ] ); //if you specify only the class, default action routing will apply //the class can be loaded via composer's autoload as well PHPPE\Http::route( "tests", "\PHPPE\Tests" );
As you may noticed in the examples above you can add filters to routes. If a filter fails, the URL will be automatically skipped. If all routes failed, and there was at least one that because of a filter, the url will be routed to 403 (access denied) page. When no routes matched at all, PHPPE\Core::run() will fall back to default route.
(base) / application [ / action [ / item ] ]
First PHPPE will check whether the url is handled by an action specific controller. Next, it will check if it's handled by a controller or service class with several action methods. As a last resort fallbacks to PHPPE Content built-in action, which in turn will look for a content in database.
After executing controller class constructor, the caching instance will be consulted. If the page can be found in cache, action method won't be called. See caching for details. On the other hand, if it's not in cache, or page generation is forced by setting PHPPE\Core::$core->nocache in constructor, action method will be executed.
If you omit action parameter from URL rule, the action() and default action() methods will be looked up in the specified class.
If no application class found, run() will fallback to act as a Content Server, serving pages from database. Contents are generated on a dedicated server by the PHPPE CMS extension. If a page for the url found, it will populate the application's properties and sets the template to be used in view. In this case action method will provide a way to execute code from database, unless it's turned off by the PHPPE\Core::$core->noctrl flag.
After an action method returns (either called by matching, default or fallback) the templater takes control over, unlike other frameworks where controller has to call the templater.
/login | Log in user (requires POST arguments). It handles admin user internally and will also call (PHPPE\Users)->login() method if found. |
/logout | Log out user and destroy session. It handles admin user internally and will also call (PHPPE\Users)->logout() method if found. |
/css, /js, /images, /fonts | Assets proxying. |
?clear | Clear ClassMap cache and destroy session, but preserve logged in user |
?cache=(hash) | Get content from cache |
?nojs | Disable JavaScript client detection |
?lang=(langcode) | Override client's language |
Override webserver's url rewrite | |
?--dump | Dump environment, only works in developer and debug mode (runlevel 2 and above) |
?benchmark | Collect benchmark data for the utility shipped with Developer. |
You can add one or more filters to pages and fields on a form, not to mention the menu items. Most common case to apply as fourth argument to PHPPE\Http::route().
If a filter name starts with a '@', the user will be checked against the given ACE, otherwise \PHPPE\Filter\name::filter() will be called
that returns a boolean value.
For an ACL list, use pipe to concatenate ACEs (like "@siteeditor|siteadmin"). If you would like to describe user has A and at least one of B or C, that would be "@A,@B|C".
PHPPE Core has four built-in filters:
You can find filter examples in the API section.
Model classes are inherited from PHPPE\Model. This way they will have three methods at least:
find() | to get a filtered list of objects of the same kind |
load() | to populate the object with a specific record's data |
save() | to save object into database record |
A typical example of a model is PHPPE\Users class.
These classes are the glue that connects HTTP requests with models and a view.
An application (a PHPPE extension) can house several controllers, and each controller can hold code for several (but at least one) action handler.
Using PHPPE\Http::route() you can specify any controller and action to run for a specific url, or you can rely on default route to choose the controller and action method.
Controllers are located under the ctrl/ directory of every extension (and in your application as well). PHPPE gives you freedom on how you want to organize them, you can have one single controller class with more action handlers, or separate classes for each action. There's no inheritance involved, but controllers should reside in the PHPPE\Ctrl namespace.
1
\PHPPE\Http::route( "myurl", "MyController", "action" );vendor/phppe/MyExtension/ctrl/mycontroller.php:
1
2
3
4
5
namespace PHPPE\Ctrl; class MyController { //default action function action( $item ) {} }
Unlike other frameworks, in PHPPE the templater is not used to generate the entire output. Instead you've library
calls to add links, meta tags, menus, stylesheets and javascripts to it. The templater is limited to the area between <body>
and </body>
only. This way PHPPE can manage asset aggregation and minification transparently.
The view is generated using the first template found in order:
If the application clears the template's name, the view layer will not be used at all, and therefore there'll be no 404 page either.
Each of the above list will be looked up in:
views table id='template' | if datasource available, views table will be checked. |
app/views/template.tpl | if no datasource connected or the template is not found in views table, application's directory will be checked. This way your application can override any view templates shipped with extensions. |
vendor/phppe/application/views/template.tpl | finally the template will be searched for in corresponding extension's directory. |
<!foreach array> ... <!/foreach>
Repeats xml block array length times. If array elements are instances or associative arrays, their fields can be referred on each interation. On scalar arrays, you may want <!=VALUE>. Also the index of the array available as <!=KEY>. For non-associative arrays, <!=IDX>, the iteration counter is one bigger than KEY. See expression tag below.
<!if expression> ... [ <!else> ... ] <!/if>
Conditional blocks. First block is parsed if the expression is true, the second block otherwise (if exists). A special expression "cms" can be used here which evaluates to true only if page is viewed in Content Editor. With this you can configure otherwise hidden page properties in Editor.
<!include view>
Insert another template's content into current one just as if it were entered here.
<!template> ... <% ... > ... <!/template>
Blocks surronded by the template tag interpreted twice. On first run <!, and <% on second. This can be used to generate template tags with templater.
<!form objectname [ cssclass [ app/action [ onsubmitjs ] ] ] > ... </form>
Starts a form for an object. There's no closing tag, use the usual </form>
(without !). Inside a form, you can refer to it's fields with <!=>
. Also, every addon can load and save it's properties.
Object can be either an instance or an associative array. If neither application/action url nor onsubmitjs specified, it will post the form to the same url as it was generated from.
Human readable format and time units are specified in translations (available in PHPPE Pack). Without that these tags will fallback to ISO date format (YYYY-mm-dd HH:MM:SS) and English units (sec, min, hour, day etc.).
<!date expression>
Display an integer (UTC UNIX timestamp) as a human readable, localized date.
<!time expression>
Similar to <!date>
, but also displays time.
<!difftime expression> <!difftime tstamp1 tstamp2>
Display the integer as difference to dates.
<!L textkey>
Translate a string to the browser's language. This is equivalent of calling L() in an expression:
<!=L(textkey, ...)>
<!dump field>
Converts object or a field to a human readble string and displays it along with it's name.
This only works if runlevel is at least 1 (verbose or testing). These level use print_r() for dumping, higher levels (developer and debug) use var_dump().
<!app>
This tag is unique. It's limited to sitebuilds at one position only.
//variable, expression or object field.
<!=expression>
No php allowed in expressions; you have to convert both property names ($obj->field) and associative arrays ($obj["field"]) to obj.field
. Also note that there's no "greater than" operator, if you plan to use, swap sides of the expression and use "less than" operator. Besides of these it works like php, you can use number_format() and sprintf() functions as well (<!=sprintf("%12d",obj.numval*2+10)>, <!=myobject.mymethod(myobject.field3)>).
Inside <foreach>
iteration blocks you can use a couple special variables as well:
Calling PHPPE Core library would require the prefix "core.", like
<!=(core.isinst("MyAddOn") && MyAddOn.mymethod())> <!=core.getval("core.lib()")>You can limit the functions if PHPPE\Core::$core->allowed array is set: only functions listed there will work, save core functions and L() which are always available. Otherwise templater will generate an E-BADFNC error if referenced function is not listed.
You can access the framework object's properties and methods with
PHPPE\Core::$core | core. | PHPPE Core |
PHPPE\Core::$user | user. | Logged in user |
PHPPE\Core::$client | client. | Client's (browser's) properties |
Controller instance | app. | Your application's controller class. Properties can be accessed without prefix as well |
Both field types and widgets are extended from PHPPE\AddOn class. The difference is how you refer to them in view templates.
//create an add-on instance for a form field.
<!field [ @acl ] [*]addon [ ( args ) ] objectname.fieldname [ attributes ]>
<!var [ @acl ] [*]addon [ ( args ) ] objectname.fieldname [ attributes ]>
<!widget [ @acl ] [*]addon [ ( args ) ] instancename [ attributes ]>
<!cms [ @acl ] [*]addon [ ( args ) ] app.propname [ attributes ]>
The reason for having four different kind of tags is to ease the creation for templates. For example with "var" you can display and edit a database record with a single template, so you won't have to create more templates for the same object. Similarly "widget" can be used to place an AddOn on the page regardless whether it's showing it's face or it's configuration. Both will use the same template.
The "field" tag will call the edit() method of the Add-On to render html content, regadless if there's a show() method or not. If you use "var" instead of "field", the show() hook will be called, edit() will be limited to edit mode only ($_SESSION['pe_e'] is true). If edit() hook is undefined, fallbacks to show(). If neither show() method found, "var" will output raw value, while "field" reports an error.
Widgets works just like "var", show() method should draw it's face, and edit() method displays a widget configuration form (optional, if $_SESSION['pe_c'] is true).
Technically "var" and "widget" are the same, the difference is limited to the meaning of what edit() renders, and when edit() gets called instead of show().
The "cms" tag will use edit() only if page opened in Content Editor. Normally it won't display anything unless it's marked mandatory (by an asterisk prefix in type), when it will use show(), just like "var". Also you can specify editor mode only templater blocks with "<!if cms>
...<!/if>
", see conditional block above. The "cms" tag is special in a way as it accepts extra dimension parameters to help intergation of the editor area into your layout more smoothly.
//autodetect editor position and size <!cms //place editor at icon in given size (pixels) <!cms(width,height) //display editor as a modal occupying given percentage of the screen <!cms(0,0,percentage)
Comparision
field | var | widget | cms | Purpose | |
---|---|---|---|---|---|
Normal | edit() | show() | show() | - / show() | Display a record with formated values to the user, also showing widget faces. |
Edit | edit() | edit() | show() | - / show() | User editable form for a record with the same view template. |
Conf | edit() | show() | edit() | - / show() | Widget configuration. |
Cms | edit() | show() | show() | edit() | Editing content on the page. |
It's easy to expand functionality with new Add-Ons, see tutorial for an example.
Caching is configured in phppe/config.php:
cache | memcache location, "(hostname)[:(port)]" or "unix:(path)" or "apc" or "files". Extensions can provide other caching mechanisms. |
nocache | Set it to true to disable caching of view output and assets (templates will be still cached). |
cachettl | View output and asset cache time to live in sec (defaults to 10 minutes) |
noaggr | set it to true to turn static css and js content aggregation off in html header |
nominify | set it to true to turn css and js minifier off |
If memcached (or any other cache alternative) can be accessed, raw templates will be always cached, and there's no flag to turn that off.
Without nocache set, templater's output and assets will be also cached for 10 minutes.
Assets added by either PHPPE\View::css() or PHPPE\View::jslib() will be aggregated into two files in memory to save HTTP requests as well.
If you want to support changing content along with dynamic data, your application must be aware. You should look for new data in your app's constructor, and set PHPPE\Core::$core->nocache to avoid view's caching and force regenerating the output.
1
2
3
'cache' => "127.0.0.1:11211", //set up memcache connection 'nocache' => false, //allow caching of generated output by default 'noaggr' => true, //turn static content aggregation offapp/ctrl/myaction.php:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace PHPPE\Ctrl; //your application's controller class class MyAppController extends MyApp { public $data; //in constructor look for new data function __construct( ) { //if new data available, disable caching of output if( MyModel::getNumberOfNewItems() > 0 ) PHPPE\Core::$core->nocache = true; } //WARNING: actions not called when output is cached! function action( $item ) { $this->data = MyModel::getItems(); //when returning from this method, templater will //regenerate output using $data and raw templates //and will save the result into the cache } }
You can use any other caching mechanism, if the extension is in PHPPE\Cache namespace, and has at least two methods:
1
2
function get( $key ) { } function set( $key, $value, $compressed = true, $ttlinsec = 0 ) { }
Zero ttl means keep key in cache forever.
If the caching mechanism requires connection, it must be initialized in the constructor, using PHPPE\Core::$core->cache as argument.
* * * * * php public/index.php cron minute
There's a wrapper class to hide all the details of cache implementations, see PHPPE\Cache.
On workers, install Pack and ClusterCli. On the management server, Pack, ClusterSrv and CMS.
$ composer require phppe/ClusterSrv
The cron minute will supervise if ClusterSrv is running. You can also trigger it from command line:
$ php public/index.php cluster server
That's simple as
$ php public/index.php cluster This node is: 10.0.0.1 MASTER Cluster nodes: Id Type Load Last seen Last viewed Name -------------- --------- ----- ------------------- ------------------- ----------------------------- *10.0.0.1 master 0 2016-09-28 02:06:20 2016-09-30 01:04:42 admin1.localdomain 10.0.0.2 slave 0 2016-09-28 02:06:20 2016-09-30 01:04:42 admin2.localdomain 10.0.0.3 lb 0 2016-09-28 02:06:20 2016-09-30 01:04:42 ns1.localdomain 10.0.0.4 worker 0 2016-09-28 02:06:20 2016-09-30 01:04:42 worker4.localdomain 10.0.0.5 worker 0 2016-09-28 02:06:20 2016-09-30 01:04:42 worker5.localdomain 10.0.0.6 worker 0 2016-09-28 02:06:20 2016-09-30 01:04:42 worker6.localdomain cluster help - this help cluster status - prints the current status cluster server - checks management daemon (called from cron) cluster takeover - force this management server to became master cluster flush - flush worker cache cluster deploy - push code to workers cluster client - (!) checks worker daemon (called from cron) cluster bindcfg - (!) generate bind configuration on lb worker
$ composer require phppe/ClusterCli
The cron minute will supervise if ClusterCli is running. You can also trigger it from command line:
$ php public/index.php cluster client
Tell ClusterCli that you want it to work as a load balancer by setting the name(s) of the subdomain(s) in
vendor/phppe/ClusterCli/config.php:1
'loadbalancer' => [ 'www', 'cdn' ],
Create a shell script under vendor/bin/cluster_lb.sh
. That will receive a bind configuration file on it's stdin, and has to save it and restart a bind server of your choosing. The format can be modified in vendor/phppe/ClusterCli/views/bind.tpl
. If possible you should use layer 2 balancing (like RFC 5798) instead of a DNS balancer for smaller latency times.
$ php public/index.php cluster This node is: 10.0.0.3 LOADBALANCER
$ php public/source.php cluster bindcfg ; GENERATED FILE, do not edit! $TTL 1d ; 1 day ttl $ORIGIN example.com. @ 1D IN SOA ns1.example.com. hostmaster.example.com. ( 2016093003 ; serial (unique to every hour) 5m ; refresh 15 ; retry 5m ; expire 5m ; nxdomain ttl ) IN NS ns1.example.com. ; in the domain IN NS ns2.example.com. ; external to domain IN MX 10 mail.another.com. ; external mail provider ; server host definitions ns1 IN A 127.0.0.3 ; dns load balancer admin IN CNAME admin2 admin2 IN A 127.0.0.1 ; current master admin3 IN A 127.0.0.2 ; standby slave www IN A 127.0.0.4 ; worker #4 www IN A 127.0.0.5 ; worker #5 www IN A 127.0.0.6 ; worker #6
$ composer require phppe/ClusterCli
The cron minute will supervise if ClusterCli is running. You can also trigger it from command line:
$ php public/index.php cluster client
$ php public/index.php cluster This node is: 10.0.0.6 WORKERThat's all.
ClusterSrv will monitor for worker loads and will tell them to stop down or start a new if required. For that to happen, we'll have to do things first:
Make sure sudo
is allowed for the following commands: poweroff
, restart
.
Make sure (openbase_dir) script is allowed to open and read /proc/loadavg.
Create a shell script vendor/bin/cluster_worker.sh
, that should communicate with your VPS provider's API to start a new worker vm instance.
You can start more load balancers, but change the name in bind.tpl for each.
As for management, ClusterSrv implements fail-over. That means that you can start more instances, but only one can be master at any given time. There may be a need to change master manually, for that run this command on one of the slave servers:
$ php public/index.php cluster takeover
Contents are stored in the quorum database server. Workers cache the resulting html for subsequent use. That cache can be cleared with:
$ php public/index.php cluster refresh
You can keep a master copy of your worker's document root on the management server. If you change that, you'll have to send it to the workers with:
$ php public/index.php cluster deployIt's called push deployment, which is done by
rsync
over a secure channel. The reason why PHPPE prefers push over poll is performance and security: you can set up one-direction firewalls
and if any of your workers get hacked, there's no way to the management zone from there. If one or more worker gets defaced, you can use this command to restore them to their original state.
Provides a low level web interface to the database.
You can list tables, search and edit records with it. It's purpose is not to be a fully featured tool rather to be a swiss-army knife for S.O.S. fixes in the database.
You need siteadm
ACE to use the DataBase Administator.
Every extension can have an init.php that holds initilization code. If that file returns an object instance, that's called a Service.
Services provide library functions to PHPPE, or can extend functionality by implementing event hooks.
If classname is not obvious, other parts of the system can ask for a service by calling PHPPE\Core::lib(servicename) which will return the instance.
1
2
//! return a class defined under Extension's libs/ directory return MyService();
There's a special template in PHPPE, that can have an <!app>
tag. This tag separates the page header (logo, header image etc.) from the page footer (copyright notice, impressum link etc.). So minimal frame is this tag alone. If you omit this tag, your application's output will not be included in the final output.
This frame usually describes the basic layout of the website: where's the menu, where's the navigation bar, where are the ads etc.
For AJAX loaded applications and popup windows you can turn the panel and the frame off, leaving the entire space to the application (full screen mode if you like) with
1
2
PHPPE\Core::$core->nopanel = true; PHPPE\Core::$core->noframe = true;
In the views table of the database there are special records marked by a site build name. These are the layouts that can be used as frame, one at a time. All of them must contain an <!app>
tag.
With PHPPE CMS you can manage views on a nice web interface, and you can select one sitebuild to be used as frame with a single click.
Along with code, usually come some static file. They can be CSS stylesheets, imagery or client side JavaScript libraries. You don't have to mess them up in a single directory under "public/", PHPPE Core is clever enough to locate these files and proxies their contents. For that, the following URLs are reserved:
Each of these has a corresponding directory within every PHPPE extension. Your application's assets are handled defferently, as they are symlinked under "public/". This means that the static contents of your application will be served by the webserver directly without PHP involved. Extension's assets (located under "vendor/" which is not accessible by the webserver) and dynamicaly generated assets have to be proxied through PHP.
For that there are two corresponding methods to add links to output's head section and libraries before the body tag:
images/
"fonts/
"These should be called in init() method of Add-Ons or action handler classes. Don't call them in service intitialization, as the referenced assets would be included in every output even if they're not needed at all.
One subdirectory is supported for grouping assets, but only one level for performance (to speed up directory lookups).
It's possible to generate assets on-the-fly. For that you'll have to append ".php" extension to the filename. When you refer these, do that without that extension, PHPPE is clever enough to handle them on it's own.
js/example1.js | PHPPE\View::jslib("example1.js"); |
js/example2.js.php | PHPPE\View::jslib("example2.js"); |
images/example1.png | link as "images/example1.png" |
images/example2.png.php | link as "images/example2.png" |
This way the output looks the same for both static and generated assets.
You don't have to do anything special in these php files, when they are included, PHPPE is already bootsrapped for you, so you can focus on generating the output. If cache is configured, by default the generated asset will be cached and served like any other static asset. If you want to disable that behaviour (for example you're generating an image with a changing counter on it), set the PHPPE\Core::$core->nocache flag and your code will be called every time the asset queried.
PHPPE Core can serve contents (generated by PHPPE CMS) on it's own, but it uses a database table for that. The scheme is supplied by PHPPE Pack (vendor/phppe/Core/sql/pages.sql).
PHPPE\Content will look for pages if routing was unable to choose a controller.
To be able to create and modify contents on a Content Server site, you'll need to install PHPPE CMS extension by copy'n'pasting one of these commands into your Terminal:
composer require "phppe/CMS"
mkdir vendor/phppe/CMS && curl https://bztsrc.github.io/phppe3/phppe3_cms.tgz | tar -xz -C vendor/phppe/CMS
It can be installed on a different server, and one CMS can manage several Content Servers. This way you can build up a scalable architecture that can serve huge loads with ease.
visitors webmaster (Browser Clients) \ / websrv+dbsrv (Content Server, \___/ Content Editor, and database as well on a single machine)
visitors webmaster (Browser Clients) | | DNS load balancer | / | \ | / | \ | websrv1 websrv2 ... websrvN | (Content Servers) \ | / cmssrv (Content Editor) \__ _ __/ / (_) ___________/ dbsrv |_| (database server)
In scalable set up the webmaster (who is responsible for the contents) uses the cmssrv with Content Editor installed to manage the webpage. This should be done through a secure https channel protected by a firewall, so that nobody else could modify the contents. The Content Editor stores the changes to the database that resides on dbsrv over a sql connection. In the cheapest scenario database server and Content Editor can be installed on the same machine (although not recommended due to security considerations. Database server should not be accessible from outside).
Visitors are accessing the site on http(s) protocol targeting a load balancer. The load balancer forwards the http(s) request to one of the websrv instances that run Apache or nginx. The cheapest and simpliest solution is to use DNS load balancing, where the domain name resolves to several A records, each pointing to one of the websrv ip's. More professional load balancers can terminate ssl and forward plain http to save resources, and may also monitor the current load on websrv instances to route to the least loaded one.
All the websrv instances have the same document root and run PHPPE Core to act as a Content Server that in turn loads the content from dbsrv via sql connection. To speed up things ever more, Content Servers can cache the contents as well.
There's also an extension to support pages generated by third party software. EplosCMS generates pre-constructed pages in the "pages/" directory, this extension read files from there and creates routing entries as they are referenced. You can install this extension with:
composer require "phppe/EplosCMS"
mkdir vendor/phppe/EplosCMS && curl https://bztsrc.github.io/phppe3/phppe3_eploscms.tgz | tar -xz -C vendor/phppe/EplosCMS
In PHPPE you don't set handlers by calling a method. Instead, you simply name your method after the event in your registered class.
Service events: | init($cfg), diag(), route($app,$action), ctrl($app,$action), view($html), stat(), cron() |
Filtering event: | filter() |
Add-On events: | init(), validate($name,$value,$required,$args,$attribs), show(), edit() |
JavaScript event: | init() |
Defined | in extension classes returned by init.php |
---|---|
Called | during the bootstrap process in PHPPE\Core constructor. |
Purpose | register libraries, menus and other stuff needed by extensions |
1
2
3
4
5
6
7
8
9
10
11
12
13
/** * Service init event handler * * @param config associative array * @return boolean, was initialization successful? */ function init($config) { // PHPPE\View::menu() to add menu items // PHPPE\View::jslib() to add JavaScript libraries // PHPPE\View::css() to add custom stylesheets return true; }
Defined | in extension classes returned by init.php |
---|---|
Called | in diagnostics mode (when called from command line with "--diag" argument) |
Purpose | do checks on extension's data integrity and consistency |
1
2
3
4
5
6
7
8
/** * Extension diagnose event handler * */ function diag() { echo("Hi\n"); }
Defined | under libs/, but outside of your extension's namespace, so use a separate file for filters. |
---|---|
Called | if the url (for which the response is being generated) matches a routing rule with filter in it, the appropriate function will be called. |
Purpose | restrict access for specific urls. |
1
2
3
4
5
6
7
namespace \PHPPE\Filter; class myFilter extends \PHPPE\Filter { function filter() { return true; } }
Defined | in any service extension. |
---|---|
Called | after routing decision is made, but controller not instantiated yet. |
Purpose | alter application's class and action method's name, expand routing capabilities. |
1
2
3
4
5
6
7
8
9
10
11
/** * Route event handler * * @param application classname * @param action method name * @return array optionally altered input */ function route($app, $method) { return [ $app, $method ]; }
Defined | in any service extension. |
---|---|
Called | before action handler gets called (NOTE: only if caching is disabled for the page). |
Purpose | expand controller functionality with application independent action code. |
1
2
3
4
5
6
7
8
9
/** * Controller event handler * * @param application classname * @param action method name */ function ctrl($app, $method) { }
Defined | in any service extension. |
---|---|
Called | before view is sent to the browser. |
Purpose | alter html output before it's displayed. |
1
2
3
4
5
6
7
8
9
10
/** * View event handler * * @param string html * @return optionally altered html */ function view($html) { return $html; }
Defined | in any service extension. |
---|---|
Called | when PE Panel is generated. |
Purpose | return html to display status icon of the service on the panel. |
1
2
3
4
5
6
7
8
9
/** * Stat event handler * * @return html */ function stat() { return "<img src='images/icon_ok.png'>"; }
Defined | in any service extension. |
---|---|
Called | from crontab, see background jobs. |
Purpose | run a scheduled background job. |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class myExtension extends \PHPPE\Ctrl { function cronMinute( $item ) { //here you can use the usual PHPPE environment. Don't forget to output text, not html. echo( L("Called.") . "\n" ); } function cronQuaterly( $item ) { //do something here echo ( $item . "\n" ); //most scripts will exit without calling the templater. } function cronHourly( $item ) { //if you want templater to continue, set a template manually in CLI scripts PHPPE\Core::$core->template = "cron_ncursestmpl"; } }
Defined | in PHPPE\AddOn classes |
---|---|
Called | once per page generation, before output is displayed. |
Purpose | register JavaScript libraries, stylesheets etc. needed by the Add-On |
1
2
3
4
5
6
7
8
9
10
/** * Add-On init event handler * */ function init() { // PHPPE\View::addon() to register Add-On // PHPPE\View::jslib() to add JavaScript libraries // PHPPE\View::css() to add custom stylesheets }
Defined | in PHPPE\AddOn classes |
---|---|
Called | when PHPPE\Core::req2arr() called in an action handler. |
Purpose | validate or sanitize user input. You can chain more Add-On validators on one input field with PHPPE\Core::validate(). |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** * Add-On validation event handler * * @param string field name * @param mixed value passed by reference * @param boolean required * @param array arguments * @param array attributes * @return array of boolean and string */ static function validate ( $name, &$value, $required, $arguments, $attributes ) { return [ true, /*boolean, expression to determine whether the value is valid*/ "success" /*error message if any*/ ]; }
Defined | in PHPPE\AddOn classes |
---|---|
Called | every time whenever a template refers to that Add-On |
Purpose | return html content |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** * Add-On display event handlers * */ function show() { // display value or widget face return htmlspecialchars($this->value); } function edit() { // display user input field or widget configuration form return "<input type='hidden' name='".$this->fld."' value='".htmlspecialchars($this->value)."'>"; }
Defined | as a second argument to PHPPE\View::jslib() calls, usually (library).init() . |
---|---|
Called | on client side as soon as the DOM can be manipulated |
Purpose | do JavaScript stuff that has to be done after the page and JavaScript libraries loaded. |
Assuming you're in your ProjectRoot. If not, type "cd (your DocumentRoot without public)". Check current working directory with "pwd".
Type one of these into your SSH Terminal:
$ mkdir vendor/(extension name) && curl (repository)/(extension tarball).tgz | tar -xz -C vendor/(extension name)
Type into your SSH Terminal:
find vendor/phppe -name composer.json -print -exec sh -c "cat {} | grep -e \\\"version[^_]" \;
The PHPPE Extensions Manager is a tool to install extensions over a web interface. It does not provide the same functionality as PHP Composer, so it's not recommended in development environments. Instead it's a comfortable and secure way to deploy tarballs remotely via web UI.
You need install
ACE to use the Extensions Manager.
To install, copy'n'paste one of these into your SSH Terminal:
composer require "phppe/Extensions"
mkdir vendor/phppe/Extensions && curl https://bztsrc.github.io/phppe3/phppe3_extensions.tgz | tar -xz -C vendor/phppe/Extensions
php public/index.php --self-update
If you have PHPPE\Users class installed with PHPPE Pack, there's little left to do, just grant "install" ACE to your user, and add "remote" configuration as an array to user's preferences. On the other hand if you're running a single user page without Users class, you'll need this to allow admin user login:
phppe/config.php:1
'masterpasswd' => password_hash("changeme", PASSWORD_BCRYPT),
In lack of user preferences, you'll also need
vendor/phppe/Extensions/config.php:1
2
3
4
5
6
7
return [ "host" => "(your server name)", "port" => "(your ssh port, optional)", "user" => "(your username)", "path" => "(your ProjectRoot, DocumentRoot without 'public')", "identity" => "(your private key)" ];
For example:
1
2
3
4
5
6
7
8
9
10
11
return [ "host" => "localhost", "port" => 22, "user" => "bzt", "path" => "/var/www/phppe3_test", "identity" => "-----BEGIN RSA PRIVATE KEY----- MIIBdgIBABKBhQCdlCD+2At47+LwSk7w1rDISnj3n9QYjCKjzuj+UtI5S4vC0Dbx [...] iCa3nxaMkW2qEmEPCd8= -----END RSA PRIVATE KEY-----" ];
The path can be relative as well. Most common configurations are:
This array will be loaded to PHPPE\Core::$user->data['remote']
and handled afterwards as if it would be a normal Users preference setting.
For security reasons, users are identified by keys. No password configured ever for remote access.
The identity field contains your private key, and you have to append it's public key pair to (user's home)/.ssh/authorized_keys. If you don't have a key yet, here's how to generate it:
$ ssh-keygen -t rsa Generating public/private rsa key pair. Enter file in which to save the key (/home/bzt/.ssh/id_rsa): Enter Enter passphrase (empty for no passphrase): Enter Enter same passphrase again: Enter Your identification has been saved in /home/bzt/.ssh/id_rsa. Your public key has been saved in /home/bzt/.ssh/id_rsa.pub. The key fingerprint is: 6f:16:8a:27:54:cf:2e:6d:37:23:30:fc:4d:d7:a2:12 bzt@localhost
Now copy the content of /home/bzt/.ssh/id_dsa to configuration, and append /home/bzt/.ssh/id_rsa.pub to authorized_keys:
$ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
PHPPE Extensions Manager is capable of managing remote hosts as well, but in this case you'll have to append your key to the remote machine's authorized_keys file.
$ cat ~/.ssh/id_rsa.pub | ssh host -p port -l user 'cat >> ~/.ssh/authorized_keys'
By default, PHPPE uses it's own repository, but you can add third-party extensions as well. You can configure repositories like
phppe/config.php:1
2
3
4
'repos' => [ "http://my.repo.com/phppe3", "http://another.repo.com/stuff/phppe3" ];
To provide your own repository, see Repository HOW-TO.
Now log in to your PHPPE site. You'll see an "Extensions" menu on the PHPPE Panel. Here you can
You'll need a plain simple webserver for that. Place the tarballs and the packages.json in DocumentRoot, and you're done.
Create gzipped tarballs with ".tgz" file extension of your extensions and add-ons. Omit leading directory, tar files should contain files relative to vendor/phppe/(extension)/.
The first file in the tarball *MUST* be composer.json. PHPPE Extensions Manager will check it and refuses to install tarballs without valid meta info. This file is a standard PHP Composer json file, with additional elements:
{ "name" => name of your extension, "version" => human readable version of your extension, "version_normalized" => comparable version of your extension, "description" => description of your extension, "maintaner" => your name and email address, "license" => license you use for your extension, defaults to LGPL-3.0+, "type" => "library" "autoload" => autoload info "dist" => "type" => "tar" "url" => link to the tarball "require" => dependencies "conf" => optional, configuration specification, "conf_X" => optional, translations for configuration keys, "price" => optional, integer price of your extension in EUR, "homepage" => optional, webshop's url where the user can buy your extension, "prio" => optional, priority when generating list in packages.json }
The keys "name"
, "description"
and "conf"
can be translated by appending underscore and language code to them, like
{ "name" => "phppe/something", "name_en" => "Some Extension", "name_hu" => "Valami BÅ‘vÃtmény", "description" => "English description, NOTE: NO _en suffix", "description_hu" => "Magyar nyelvű leÃrás", "conf_en" => "{ "host":"Host name", "port":"SSH Port" }, "conf_hu" => "{ "host":"Szerver neve", "port":"SSH Port" }, }
The configuration prototype (keys of the array returned by it's config.php) can be described in "conf" field. Labels for them will be translated, see above.
For example:
"conf":{ "host": "string(localhost)", "port": "integer(1,65535,22)", "user": "string(username)", "identity": "string", "path": "sting(/var/www/localhost)" }, "conf_en":{ "host": "Hostname", "port": "SSH Port", "user": "Username", "identity": "Identity file", "path": "ProjectRoot" },
This should be a 96 x 96 png picture or an svg image. Try to keep it small, below 8k. Use color palette or grayscale instead of RGB. Always use png tools such as TinyPNG to compress.
Preview picture is optional, if not included, default extension picture will be shown.
Versions should be threefold, each number represents specific version in order
For example:
0.0.0 - first beta release
0.1.0 - second version of beta release
1.0.0 - first stable release, usable for production
1.0.1 - first stable release with a security patch applied
2.1.3 - second main version with a feature release and three security patches
Upcoming feature and main releases required to include former security patches.
{ "packages" => (extension) => (version_normalized) => content of composer.json "time" => last modification time in "yyyy-mm-dd HH:MM:SS" format "size" => size of the tarball in bytes "sha1" => hash of the tarball "preview" => optional, base64 encoded png or svg icon, generated from preview file (extension) => (version_normalized) => content of composer.json ... more extension info may follow }
PHPPE does not include a shop for Extensions to avoid legal issues and taxing regulations in different countries.
PHPPE Extensions Manager merely helps commertial extensions with a token in download url during installation, but that's all.
If you specify "price" in your composer.json, then "homepage" must point to a webshop. PHPPE Extensions Manager will handle extensions with price like:
To be extremely small (90k is not a plenty of space for a PHP code you can take my word on that), the Core is compressed. With this only exception, all other PHP files must follow PSR-2 Coding Style.
It is strongly advised to write plenty of descriptive comments in your source. It is also highly recommended to put a lot of debug level PHPPE\Core::log() call, so that you'll get a clue what is your code doing when it's running.
This document uses these notations consistently:
class:: | static property or method |
(class)-> | property or method of an instance of "class" |
Class names follow StudyCaps. To clearly distinguish Add-Ons from other classes, they're in camelCaps, like "myWidget". Don't ever use underscore in your class names.
Method names are in camelCase. Again, don't use underscores in your method names.
Along with classes and methods, PHPPE Core also provides two widely used functions outside of it's namespace (which by the way are only wrappers around PHPPE properties and method calls) for simplicity and to save you typing:
The last two are often called in view templates.
As the framework allows controllers in applications (app) as well as in extensions (vendor/phppe/*), you may ask which one to use for your project?
Short answer: application. If you later on plan to use your code on several sites, it's easy to convert it into an extension as they share the same directory structure. The only difference is that an extension must be inside PHPPE namespace, an application is not limited by that.
PHPPE performance is a very important thing. Therefore you'll find a built-in benchmarker.
Append "?benchmark
" to the page's url you want to measure. Reload it several times, more samples are the merrier.
PHPPE Developer extension ships a nice web interface to aggregate and display the collected data. If everything went well, you should see something similar to this:
Name | Start | Time consumed |
---|---|---|
phppatch | 0.000000 | |
getconfig | 0.000055 | |
autoload | 0.000213 | |
init | 0.002741 | |
tokens | 0.003818 | |
viewinit | 0.003865 | |
routing | 0.004056 | |
cache | 0.004246 | |
controller | 0.005117 | |
action | 0.005132 | |
view | 0.005717 | |
header | 0.028299 | |
content | 0.028454 | |
footer | 0.028579 | |
Total | 0.02867 | secs |
Name | Min | Avarage | Max | ||
---|---|---|---|---|---|
phppatch | 0.000042 | 0.000055 | 0.000078 | ||
getconfig | 0.000119 | 0.000158 | 0.000232 | ||
autoload | 0.002089 | 0.002528 | 0.003307 | ||
init | 0.000910 | 0.001077 | 0.001267 | ||
tokens | 0.000038 | 0.000047 | 0.000055 | ||
viewinit | 0.000164 | 0.000191 | 0.000244 | ||
routing | 0.000154 | 0.000190 | 0.000250 | ||
cache | 0.000604 | 0.000871 | 0.001178 | ||
controller | 0.000012 | 0.000015 | 0.000018 | ||
action | 0.000500 | 0.000585 | 0.000751 | ||
view | 0.022353 | 0.022582 | 0.022750 | ||
header | 0.000145 | 0.000155 | 0.000166 | ||
content | 0.000118 | 0.000125 | 0.000134 | ||
footer | 0.000090 | 0.000091 | 0.000091 |
Neat, huh? :-)
The templates can be found in vendor/phppe/Developer/templates/ directory. Each file has a JSON header and an optional payload.
{ "desc_(lang)": "Description in the given language", "args": [ array of variable names ], "dirs": [ array of directory names to create ], "package": [ "template":{variable overrides} ], "file": "file/to/write", "append": "file/to/append/to" } payload
Description is one line long. Arguments are inherited from the environment, and can be referenced as:
With "dirs", you can create additional empty directories. Note that directories to files are generated automaticaly.
To include other templates, use the "package" keyword. You can override variables passed to the template.
Finally, one of "file" or "append" may exists. Former creates a new file out of payload, the latter one appends the payload to an existing content.
The payload is parsed, and all variables will be subtituted. You also have a handful of build-in variables:
You're allowed to use variables in the JSON with all right side expressions as well.
Example
{ "desc_en": "Example template", "args": [ "extension", "class" ], "file": "vendor/phppe/@Extension@
/libs/@Class@
.php } // This is a payload for@EXTENSION@
. // Author:@@USERNAME
,@@RFCDATE
class@CLASS@
{ }
You see, just like most of the developers, I'm a lazy one. I'd rather spend an hour developing a small utility for a simple task knowing it will be at hand next time needed, than repeating the same process over and over again. So one of the main goal was to make life easier and development effective. Simple, consistent directory structure, useful API, following the standards like PSR-2 etc. So even if you don't want to contribute to the framework, just using it or writing your own Extensions, it's worth installing the Developer package!
To install, copy'n'paste one of these into your SSH Terminal:
composer require "phppe/Developer"
mkdir vendor/phppe/Developer && curl https://bztsrc.github.io/phppe3/phppe3_developer.tgz | tar -xz -C vendor/phppe/Developer
PHPPE suggests 3 layers:
Provides a PHPUnit compatible test environment. This allows you to run tests from a browser, but nothing more: it's not as featureful as PHPUnit, for example it cannot generate code coverage reports.
The PHPUnit_Framework_TestCase
compatibility class can be found in vendor/phppe/Developer/libs/phpunit.php, and thanks to autoloading, it will be used transparently if the real PHPUnit class not found.
The advantage over PHPUnit is that it scans all extensions for test cases, so you will see all of them in one place and can run all at once.
require-dev
in the original extension's composer.json file.Developer ships a nice and pleasant reporting tool for visualizing benchmark data. It aggregates the accumulated samples and shows the result on human readable charts.
This is the one that probably you'll use the most. It's a small templater to generate different files for the PHPPE environment. The templates can be found in vendor/phppe/Developer/templates/ directory, and you can add new ones any time you want. The format is plain simple, just take a look into one. Templates can be listed if you don't provide template name:
$ php public/index.php create Usage: php public/index.php create <template> [arg1 [arg2 [...]]] Template can be one of: action action template addon creates a view addon (widget) composer create composer.json package information config configuration file controller controller template extension create an extension filter url filter template init extension initialization lang configuration file lib library template model model template phpunit.xml creates xml configuration for PHPUnit route adds a new route scheme SQL table scheme test test case for a class update SQL update view create a view
As you can see, there's a template for almost everything you'll need. For example if you want to create a new extension, just name it, and the rest will be taken care of for you.
$ php public/index.php create extension MyService Writing vendor/phppe/MyService/composer.json Writing vendor/phppe/MyService/config.php Writing vendor/phppe/MyService/init.php Writing vendor/phppe/MyService/sql/myservicemodel.sql Writing vendor/phppe/MyService/lang/en.php Writing vendor/phppe/MyService/libs/MyService.php Writing vendor/phppe/MyService/libs/MyServiceModel.php Writing vendor/phppe/MyService/ctrl/myservice.php Writing vendor/phppe/MyService/view/myservice.tpl Writing vendor/phppe/MyServiceDev/tests/MyServiceTest.php Writing vendor/phppe/MyServiceDev/phpunit.xml
The testing framework also available from CLI.
$ php public/index.php tests Test boundle Avg.time #Tests Last run Result ------------------- -------- -------- -------------------- ---------- MultilingualTest 0.0002s 2 / 2 2016-06-01 00:46:38 OK SystemTest 0.2199s 55 / 55 2016-06-01 00:46:38 OK AddonTest 0.0028s 40 / 40 2016-06-01 00:46:38 OK RoutingTest 0.0208s 22 / 22 2016-06-01 00:46:38 OK CacheTest 1.3239s 18 / 18 2016-06-01 00:47:15 OK HttpTest 1.0810s 16 / 16 2016-06-01 00:46:40 OK ConvertTest 0.0035s 23 / 23 2016-06-01 00:46:40 OK DataSourceTest 0.0152s 50 / 50 2016-06-01 00:46:40 OK TemplaterTest 0.1184s 82 / 82 2016-06-01 00:46:40 OK PictureTest 0.4101s 30 / 30 2016-06-01 00:46:41 OK DBTest 0.0047s 28 / 28 2016-06-01 00:46:41 OK EmailTest 2.0121s 13 / 13 2016-06-01 00:46:43 OK RegistryTest 0.0055s 6 / 6 2016-06-01 00:46:43 OK UsersTest 0.0001s 2 / 2 2016-06-01 00:46:43 OK
shows the latest results, and you can run one or all test with:
$ php public/index.php tests run testfile $ php public/index.php tests run
You can use this utility to format all your PHP sources in an extension to PSR-2 Coding Style.
$ php public/index.php pretty extension
It is a useful helper that scans your extension directory looking for translation references.
$ php publix/index.php lang extension
It can also merge the result with an existing language dictionary, prefixing unnecessary items by //-
.
$ php publix/index.php lang extension languagecode
If you want to save the results, it can be done as easy as
$ php publix/index.php lang extension languagecode --write
This small tool will generate data URI for images or icons.
$ php publix/index.php datauri file
This small tool is very useful if you create your own repository let's say for your company's applications. This will scan all extensions under vendor/phppe/ directory of your developer environment collecting information from composer.json files. If source changed, it re-generates tarballs and saves packages.json as described in Repository section of this document. Now all you have to do is copy the latter along with the tarballs onto a webserver, specify the url in your production server's configuration and your deployment system is good to go!
$ php public/index.php mkrepo Scanning for packages: 20 found CMS: tgz OK ClusterCli: tgz OK ClusterSrv: skip OK CookieAlert: skip OK Core: tgz OK DBA: skip OK DataLibrary: skip OK Developer: tgz OK EplosCMS: skip OK Extensions: tgz OK GPIO: tgz OK Gallery: skip OK GeoLocation: skip OK POS: skip OK YouTube: skip OK bootstrap: skip OK bootstrapcdn: skip OK og: skip OK spectrum: skip OK wyswyg: skip OK Saving packages info: OK
It's job is to compress the human readable, commented public/source.php into public/index.php, a minified, production ready format and to update the documentation you're reading now. It's also fires a minify event to compress 3rd party files, although PHPPE Core ships with an on-the-fly js and css minifier.
$ php public/index.php minify Compressing index.php: aa1c80932e73551a9816f7b84fabe43622c5adf4 80782 (0) OK Updating documentation: OK
We'll only use view layer, no controller at all. If PHPPE does not find any application and controller, it will only serve the view. This behaviour is ideal for pages like About Us and Impressum. Default routes apply here.
app/views/helloworld.tpl:<h1>Hello World!</h1>
Now open a browser tab and go to (your homepage)/helloworld url to see the result.
A very minimal application, with only one controller. The view here refers to an application property.
app/views/helloworld.tpl:<h1><!=hello></h1>
The application does not set any route, it uses the standard class naming convention for default routing. Also, action uses default action route.
app/ctrl/helloworld.php:1
2
3
4
5
6
7
8
9
namespace PHPPE\Ctrl; class HelloWorld { public $hello; function action( $item ) { $this->hello = "Hello " . ( $item ? $item : "World" ) . "!"; } }
Assuming we want to serve the page in an arbitrary PHPPE Extension.
vendor/phppe/MyExtension/views/myView.tpl:<h1>Hello World!</h1>
This time we'll set a routing rule for that, because the url does not align with file and class naming. It has to be done in the extension's initialization code.
vendor/phppe/MyExtension/init.php:1
PHPPE\Http::route("helloworld", "MyExtension", "myAction");
Here's the controller, that sets the template's name for the view. No default action route here, we've specified the action method with route.
vendor/phppe/MyExtension/ctrl/MyExtension.php:1
2
3
4
5
6
7
namespace PHPPE\Ctrl; class MyExtension { function myAction($item) { PHPPE\Core::$core->template = "myView"; } }
Here we'll create a widget to display "Hello World". View refers to our widget without any specific controller.
app/views/*.tpl:
<!widget helloworld>
- or -
<!field helloworld obj.field>
This will be the template that refers to our widget. Without reference, our widget never get's loaded.
app/addons/helloworld.php:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace PHPPE\AddOn; class helloworld extends \PHPPE\AddOn { function init() { //! register our add-on PHPPE\Core::addon( "helloworld", "Hello World! Tutorial" ); } function show() { return "Hello " . ( $this->value ? $this->value : "World" ) . "!"; } function edit() { return "Hello <input type='text' value='" . htmlspecialchars($this->value) . "'>!"; } }
Now we have a widget face to draw, and a configration "form" where we can set who to greet.
This is the simpliest, the view will be directly mapped to the url. For an example, see Hello World! tutorial.
If no route given, or neither of them applies, PHPPE will fallback to default route. This is not the scope of this tutorial either.
Adding application to routes, but still relying on default action route. See PHPPE\Http::route() documentation on how to specify routes.
app/init.php:1
PHPPE\Http::route( "myurl", "MyApp" );
The corresponding controller:
app/ctrl/MyApp.php:1
2
3
4
5
6
7
namespace PHPPE\Ctrl; class MyApp{ function action($item, $item2="") { //! something } }
This will be served when (base)/myurl called. If you enter (base)/myurl/myaction in the browser, PHPPE will look for the method myaction($item)
in this class first, then fallbacks to action($item)
.
1
PHPPE\Http::route( "myurl", "MyApp", "myAction" );
The corresponding controller now uses a custom method name for action:
app/ctrl/MyApp.php:1
2
3
4
5
6
7
namespace PHPPE\Ctrl; class MyApp{ function myAction($item) { //! something } }
This time we'll use pharenthesis in url regexp. Each corresponds to an action method argument:
app/init.php:1
PHPPE\Http::route( "myurl([0-9]*)/([a-z]+)", "MyApp", "myAction" );
And the controller:
app/ctrl/MyApp.php:1
2
3
4
5
6
7
namespace PHPPE\Ctrl; class MyApp{ function myAction($someNumber, $someLetters) { //! something } }
Without pharenthesis, the url will be splitted up by slashes '/', and each passed as an argument.
(base) / app / action / item:
1
function action($item)
1
function action($item1, $item2)
You can add a filter list as fourth argument to PHPPE\Http::route(). As any other list in PHPPE, it can be a comma separated string list or an array.
Filters that starts with a '@' excpets the user to have an ACE, others refer to a boolean logic placed in PHPPE\Filter\name::filter().
1
2
PHPPE\Http::route( "myurl([0-9]*)/([a-z]+)", "MyApp", "myAction", "@loggedin,post" ); PHPPE\Http::route( "myurl([0-9]*)/([a-z]+)", "MyApp", "myAction", ["@loggedin", "post"] );
Normally you would use one action handler for each url, and check for csrf-valid form post in there with PHPPE\Core::isBtn(form name). But, because PHPPE gives you maximum flexibility, you can also have a separate method:
1
2
PHPPE\Http::route( "myurl([0-9]*)/([a-z]+)", "MyApp", "myGetAction", "get" ); PHPPE\Http::route( "myurl([0-9]*)/([a-z]+)", "MyApp", "myPostAction", "post" );
What's more, if there's only one form on your page, you can spare the formentioned isBtn() call as well:
1
PHPPE\Http::route( "myurl([0-9]*)/([a-z]+)", "MyApp", "myPostAction", "post,csrf" );
This tutorial will show you a typical application page. It will teach you how to create a form, tide it to an object so that their values get filled in automatically, and finaly how to retrive the user provided data with validation.
Let's assume we're using the url (base)/tutorialapp and (base)/tutorialapp/myaction. For details see routing. In this tutorial, it's enough to know we're relying on default routing.
Now, create the views for our urls with forms in them.
app/views/tutorialapp.tpl:Tutorial default action's view <!include errorbox> <!-- include a box to show error messages --> <!form tutobj> <!-- assign form to an object --> <!field text tutobj.field1> <!field num tutobj.field2> <!field update> </form>app/views/tutorialapp_myaction.tpl:
Tutotiral myaction view <!form tutobj> <!field text tutobj.field1> <!field update> </form> <!form tutobj2> <!field num tutobj2.field1> <!field update Save> <!field update Save_and_New> </form>
And here comes the controller.
app/ctrl/tutorialapp.php:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
namespace PHPPE\Ctrl; class TutorialApp { public $tutobj = null; public $tutobj2; function action( $item ) { //! This flag is set on valid form submit once, //! making the transaction unrepeatable. if ( PHPPE\Core::isBtn() ) { //! Add an extra validation rule to some field PHPPE\Core::validate('tutobj.field2', 'myvalidator'); //! Get the form fields and their values from user request. //! This will call field validators automatically. $this->tutobj = PHPPE\Core::req2obj('tutobj'); //! Here you can force more complex validation or business logic if you like if ( $this->tutobj->field1=="" && $this->tutobj->field2==1 ) PHPPE\Core::error( L("Validation failed."), "tutobj.field1" ); //! If passed validation, we can save the data in database if ( !PHPPE\Core::isError() ) { //! Log what we're going to do PHPPE\Core::log('A', "Saving " . PHPPE\Core::obj2str($this->tutobj), "tutorialapp" ); //! This should be in a Model, but this is a basic tutorial PHPPE\DS::exec( "INSERT INTO tutorialapp SET " . PHPPE\Core::obj2str( $this->tutobj ) . ";" ); //! To avoid "Page expired" popups, you have to redirect user here PHPPE\Http::redirect(); } } //! Here's the right place to load object from database. //! But only if the object is not full with user input that failed validation. if ( $this->tutobj === null && !empty($item) ) //! This should be in a Model, but this is a basic tutorial $this->tutobj = PHPPE\DS::fetch( "*", "tutorialapp", "id=?", "", "", [ $item ] ); } function myaction ($item) { //! only for the first form if ( PHPPE\Core::isBtn( "tutobj" ) ) { //! This is our own $tutobj, differs from action()'s. $this->tutobj = PHPPE\Core::req2obj( 'tutobj' ); } //! only for the second form, and only if second button pressed if ( PHPPE\Core::isBtn("tutobj") == 2 ) { //! another form $this->tutobj2 = PHPPE\Core::req2arr( 'tutobj2' ); } }
1
2
3
4
//Configuration return [ "dosomething" => true ];
1
2
3
return [ "myservice_helloworld" => "Hello World: " ];
The initialization file will be loaded for each extension. This is the place to set routes and return service instance:
vendor/phppe/MyService/init.php1
2
3
<?php PHPPE\Http::route("myservice/myaction/(0-9]+)/([a-z]+)", "MyService", "myMethod"); return myService;
These class will be used by applications for services. It also acts a controller.
vendor/phppe/MyService/libs/MyService.php:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php class MyService { public $version = "1.0.0"; private $dosomthingextra = false; //! some service your extension provides function exampleService( $a, $b, $c ) { return ( L("myservice_helloworld") . round( $a + $b - $c ) ); } //! initialize our service. This method gets config.php as argument function init($cfg) { //! Register our service with dependency check //! this is not a must, it's enough to return an instance from init.php PHPPE\Core::lib( "MyService", //! unix name $this, //! service reference [ "DB", "Users" ], //! dependencies ); //! parse and validate your configuration here if ( !empty( $cfg["dosomething"] ) ) $this->dosomethingextra = true; //! Do any extension specific tests here, use PHPPE\Core::log() to indicate errors if ( $this->dosomething ) { //! Do feature specific tests here. If some test fails, //! Use PHPPE\Core::log('E',...) for high level services, //! if your service called by applications directly (no other service depends on it) //! Use PHPPE\Core::log('C',...) for low level services, //! if you expect other services dependenting on your service ... PHPPE\Core::log ( 'D', "Feature dosomething initialized." ); } else PHPPE\Core::log ( 'D', "Feature dosomething disabled." ); //! register menu on PHPPE Panel PHPPE\View::menu( L( "MyService" ), "myservice/myaction" ); } //! register a status icon on PHPPE panel function stat() { echo( "<img alt='That is me!" . ( $this->dosomething ? " ds" : "" ) . "'>" ); } }
In runtime you can access the service as simply as
1
PHPPE\Core::lib("MyService")->exampleService(1, 2, 3);
See templater for details.
You can use this Add-On in view templates as:
<!widget tutorialwgt(1,2,3,4,5,6) instancename>
The "widget" tag calls show(), unless configuration mode ($_SESSION['pe_c']) is true in which case it renders configuration panel by calling edit() hook. The "var" tag calls show(), unless edit mode ($_SESSION['pe_e']) is true.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
namespace PHPPE\AddOn; class tutorialwgt extends \PHPPE\AddOn { //! initialize - this is used by view template editor to get configuration for this widget //! also called right before the first "show" or "edit" method call once function init() { //! This code is instance independent, and called only once per page generation //! Include widget specific stylesheet PHPPE\View::css( "tutorialwgt.css" ); } //! function to draw widget face function show( ) { //! Include widget's JavaScript - has to be done here since it requires instance name PHPPE\View::jslib( "tutorialwgt.js", "pe.tutorialwgt.init('" . $this->name . "');" ); //! $this->args = [ 1, 2, 3, 4, 5, 6 ]; //! $this->attrs = [ $opts ]; return "face"; } //! function to create widget configuration form //! OPTIONAL, leave it empty if there's no configuration at all function edit( ) { return "<input type='text' onchange='tutorialwgt_saveconfig();' class='reqinput' ". "name='" . $this->fld . "_cfgopt1' value=''><br>\n". "<input type='check' onchange='tutorialwgt_saveconfig();' name='" . $this->fld . "_cfgopt2'> ". L( "tutorialwgt_cfgopt2" ); } }
This is quite straightforward. Download image and use dd
utility.
You'll need at least stretch for PHP 7.0.
You'll need these:
# apt-get install lightdm nginx php7.0-cli php7.0-fpm php7.0-opcache php7.0-apcu php7.0-gd php7.0-sqlite openbox chromium
Many of these may have already been installed on your Raspberry, and you can choose alternative db drivers like php7.0-pdo_mysql as well.
For jessie PHP 7 backports, you'll find a lot of resouce on the internet, just google. Note that jessie may not include chromium, so you'll have to use this for RPi2:
https://launchpad.net/ubuntu/precise/armhf/chromium-browser
https://launchpad.net/ubuntu/precise/armhf/chromium-codecs-ffmpeg-extra
You can find many tutorials on how to do this, but here's a brief configuration:
/etc/nginx/sites-available/defaultserver { listen 80 default_server; root /var/www/localhost/public; server_name _; location / { try_files $uri $uri/ =404; index index.php; if (!-e $request_filename){ rewrite ^(.*)$ /index.php; } } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_param SERVER_NAME $host; fastcgi_pass unix:/var/run/php-fpm.sock; } }Basically we pass control to php-fpm and we do a little rewrite trick.
echo "<?php phpinfo();" >/var/www/localhost/public/index.phpNow enable and start the service
systemctl enable nginx systemctl start nginx
Add webserver's user to gpio group:
# addgroup www-data gpio
Also, you should add /sys/class/gpio
and /proc/cpuinfo
to open_basedir in your php.ini file as GPIO Extension uses the sys kernel interface and it detects the pin layout from the revision number.
autologin-user=pi autologin-user-timeout=0Alternatively you can use raspi-config to get the job done for you.
export DISPLAY=":0" xset s off xset s noblank xset -dpms xdotool mousemove 8192 8192 openbox --sm-disable & sudo bash -c "echo '[info] phppe: Starting fullscreen browser' >>/dev/console" bash -c "while true; do (ps u|grep [c]hromium >/dev/null) || chromium -kiosk --incognito --start-maximized http://localhost; done"First line sets the display to be used, the x* commands turn screen blanking off and hides the mouse pointer. For fullscreen we need a window manager to accept fullscreen request, in this tutorial I've used openbox since it's the default. The line after that logs a message on console so that you will know when it's executing (Raspberry's bootup process is slow). And finally, the last line starts a fullscreen browser and makes sure of it that it would respawn if crashed.
<?xml version="1.0" encoding="UTF-8"> <openbox_config xmlns="http://openbox.org/3.4/rc" xmlns:xi="http://www.w3.org/2001/XInclude"> <applications> <application class="*"> <focus>yes</focus> <fullscreen>yes</fullscreen> <decor>no</decor> </application> </applications> </openbox_config>
'db' => "sqlite:data/sqlite.db", 'cache' => "apc",
You should see the PHPPE Self Test and Cheat Sheet page in fullscreen after reboot, served by your own Raspberry!
If you want to control your Raspberry Pi from your PHPPE Application (of course you do), you can use the GPIO Extension as simply as:
PHPPE\GPIO::mode(pin, 'in'/'out'); PHPPE\GPIO::get(pin); PHPPE\GPIO::set(pin,true/false);
For this tutorial, you'll need at least 5 virtual machines:
Install a webserver and PHPPE on all nodes, except database server.
php public/index.php --diag $ curl https://bztsrc.github.io/phppe3/phppe3_core.tgz | tar -xz -C vendor/phppe/Core
On the management servers, also install Extension Manager, Content Editor and Cluster Server:
$ mkdir vendor/phppe/CMS && curl https://bztsrc.github.io/phppe3/phppe3_cms.tgz | tar -xz -C vendor/phppe/CMS $ mkdir vendor/phppe/Extensions && curl https://bztsrc.github.io/phppe3/phppe3_extensions.tgz | tar -xz -C vendor/phppe/Extensions $ mkdir vendor/phppe/MultiServer && curl https://bztsrc.github.io/phppe3/phppe3_multiserver.tgz | tar -xz -C vendor/phppe/MultiServer $ mkdir vendor/phppe/ClusterSrv && curl https://bztsrc.github.io/phppe3/phppe3_clustersrv.tgz | tar -xz -C vendor/phppe/ClusterSrv
Finally, on the workers only Cluster Client:
$ mkdir vendor/phppe/ClusterCli && curl https://bztsrc.github.io/phppe3/phppe3_clustercli.tgz | tar -xz -C vendor/phppe/ClusterCli
On one of the workers modify the configuration and add subdomains you're want to load balance for:
vendor/phppe/ClusterCli/config.php:
1
'loadbalancer' => [ 'www' ],
To allow PHPPE to refresh the DNS, create a shell script:
vendor/bin/cluster_lb.sh:
#!/bin/bash exec >/var/lib/bind/myzone.conf systemctl reload bind
On all workers,
vendor/phppe/config.php
'syslog' => true
Configure your syslog daemon to send the logs to the active management server: admin.myzone
.
On all virtual machines,
vendor/phppe/config.php
'db' => 'mysql:host=quorum4.myzone;dbname=phppe;charset=utf8@dbuser:dbpass'
On the database server, install and configure mysql or any other compatible database server (like mariadb). The best if your virtual machine provider has database service as well, like Amazon AWS. There you don't have to care about the database server.
On all workers, set up php.ini to store session data in database on quorum4.myzone
.
On all workers,
vendor/phppe/config.php
'mailer' => 'db'
On the management servers, add
vendor/phppe/config.php
'realmailer' => 'smtp:gmail-smtp-in.l.google.com'as well.
On all nodes, add to your crontab
* * * * * /usr/bin/php /var/www/public/index.php cron minuteOn workers that will call
public/index.php cluster client
, on management nodes public/index.php cluster server
.
On the standby management server, type
$ php public/index.php cluster takeoverYou should see that within a minute the
admin.myzone
CNAME should point to admin3.myzone
.
With the Gallery and CMS extensions it's pretty easy.
First you'll have to load the names of the images. In order to do that, add a Local DDS to the page:
Name: carousel
, SELECT: id,ordering
, FROM: img_list
, WHERE: list_id='@ID'
, ORDER BY: ordering
The Gallery ships a template, all you have to do is include it
<!include carousel>
in your view where you want the carousel to appear. In CMS editor mode it will show a red icon, and if you click on it, you'll be able to select and rearrange pictures for the carousel.
This is the main class of PHPPE. One singleton instance can be always accessed with PHPPE\Core::$core
.
Some properties are run-time generated, others can be configured.
PHPPE\Core::$core->title | title of the site or name of the project |
PHPPE\Core::$core->base | Domain name used by url(). It's autodetected if not given. |
PHPPE\Core::$core->meta | Site global meta tags. |
PHPPE\Core::$core->link | Site global link tags. |
PHPPE\Core::$core->runlevel | current runlevel |
PHPPE\Core::$core->syslog | if true, no log files will be written but messages will be sent to syslog |
PHPPE\Core::$core->trace | if true, debug trace will also be written along log messages |
PHPPE\Core::$core->timeout | Session timeout in seconds |
PHPPE\Core::$core->sessionvar | Override session variable name (optional, defaults to "pe_sid") |
PHPPE\Core::$core->secsession | Only use session on secure channel (https only cookie) |
PHPPE\Core::$core->mailer | Email backend, used by PHPPE Pack's Email sender, like "smtp://[(user)[:(pass)@]](server)[:(port)][/(sender email address)]" |
PHPPE\Core::$core->realmailer | If Email backend is a queue in database ($core->mailer = "db"), sendmail script uses this backend when consuming mails from queue. |
PHPPE\Core::$core->nocache | Set it to true to force refresh in templater and avoid asset caching |
PHPPE\Core::$core->noaggr | Set it to true to turn static css and js content aggregation off in html header |
PHPPE\Core::$core->nominify | Set it to true to turn css and js minifier off |
PHPPE\Core::$core->cache | Connection url for caching |
PHPPE\Core::$core->cachettl | View output and asset cache time to live in sec |
PHPPE\Core::$core->db | Primary datasource |
PHPPE\Core::$core->noctrl | Disable dynamic content controller scripts |
PHPPE\Core::$core->allowed | list of allowed functions in a view template, leave it unset for no restrictions |
PHPPE\Core::$core->disabled | list of disabled Extensions |
PHPPE\Core::$core->blacklist | list of unwanted domains (for example in email addresses) |
PHPPE\Core::$core->repos | array of third-party repository urls. For details, see Repositories |
PHPPE\Core::$core->output | Output format, defaults to "html" |
PHPPE\Core::$core->masterpasswd | Encrypted password hash for superadmin (works even if PHPPE\Users class does not exists) |
PHPPE\Core::$core | PHPPE Core instance (self reference) |
PHPPE\Core::$user | PHPPE Users instance, logged in user |
PHPPE\Core::$client | PHPPE Client instance |
PHPPE\Core::$core->id | Magic, PHPPE version (read-only) |
PHPPE\Core::$core->url | Page specific part of the url (read-only) |
PHPPE\Core::$core->app | current application, (read-only, but see route event) |
PHPPE\Core::$core->action | current action (read-only) |
PHPPE\Core::$core->item | current item or null |
PHPPE\Core::$core->template | Template's name used by view layer. Defaults to "(application)_(action)" |
PHPPE\Core::$core->now | current UNIX timestamp, from primary datasource if available |
PHPPE\Core::$core->noframe | If not set, application's output will be placed in a frame |
PHPPE\Core::$core->fm | File max size |
PHPPE\Core::$core->form | Name of the submitted form if PHPPE\Core::isBtn() returns non-zero |
PHPPE\Core::$core->try | On submit the form's pressed button's serial, query with PHPPE\Core::isBtn() |
PHPPE\Core::$core->error | array of error messages, see PHPPE\Core::error() and PHPPE\Core::isError() |
PHPPE\Core::$core->libs | List of loaded libraries, register and query with PHPPE\Core::lib() |
PHPPE\Core::$core->sec | True if called over ssl and channel is secure |
PHPPE\Core::$started | Framework start time in microsecs, required by monitoring stats |
PHPPE\Core::$v | Validator data |
PHPPE\Core::$paths | Paths to extensions (important if file system is case-sensitive) |
__construct() | PHPPE Core constructor. Called by public/index.php |
bootdiag() | Run diagnostics mode |
run() | Run the current application |
static lib() | Query and register libraries (services) |
static isInst() | Checks if an extension or Add-On is installed |
static lang() | Load language dictionary for an extension in current language |
static log() | Log a message |
static isBtn() | Checks if user tries to save a form with valid CSRF token by pressing a button |
static error() | Query and register error messages |
static isError() | Checks if there was an error |
static event() | Trigger a specific event |
static validate() | Add a validator on a form field |
static req2arr(), req2obj() | Convert user request into an associative array or object |
static arr2str(), obj2str() | Convert an associative array or object into a quote-safe string |
static val2arr() | Convert a templater expression into an array |
static tre2arr() | Flat a tree (recursive array, sub-levels in "_") into a simple one level array |
static bm() | Set a benchmark point. Only works with source.php |
To save values accross pages, the Core uses some SESSION variable.
$_SESSION['pe_e'] | True if user logged in and turned edit mode on (templater var tags) |
$_SESSION['pe_c'] | True if user logged in and turned widget configuration mode on (templater widget tags) |
$_SESSION['pe_u'] | Logged in user |
$_SESSION['pe_s'] | Security tokens |
$_SESSION['pe_v'] | Validator data (filled by PHPPE\View::template(), read on post by PHPPE\Core:req2obj()) |
$_SESSION['pe_r'] | Set if PHPPE\Http::redirect() should go back to referer (return url after authentication) |
This class takes care of autoloading. It's bullet-proof and PSR-0 / PSR-4 compatible. It stores the map in JSON format because in PHPPE webserver is not allowed to write PHP files. The map is automatically regenerated if an extension is installed or removed.
.tmp/.classmap
.tmp/.acemap
static string $file | Name of the cache json file (.tmp/.classmap) |
static string $ace | Name of the ace cache file (.tmp/.acemap) |
static array $map | The generated map, used by spl_autoload_register closure |
static has() | Check if a class or method exists |
static ace() | Return Access Control Entry map. |
static map() | Return Class map. |
This class holds every information on client. It's populated consistently in CLI as well in web mode, so you don't have to bother yourself with different $_SERVER keys.
string $ip | Client's remote address (valid for forwarded requests and server behind proxies as well or "CLI" if called from command line) |
string $agent | Client's agent (browser or terminal type) |
string $user | Client's user (HTTP Authenticated user or UNIX user) |
string $tz | Client's timezone (browser's or from environment variable TZ or /etc/localtime) |
bool $js | True if client has JavaScript enabled |
string $lang | Client's preferred language's code on two characters (from request or environment variable LANG) |
array $screen | Client's screen [width, height] in pixel (or in characters, from tput utility) |
array $geo | Client's geo location (as returned by php's geoip_record_by_name(), provided by phppe/GeoLocation extension) |
None.
Object Relational Mapper prototype that correlates each record in database to a Model instance. Should not be used directly, instead inherited.
int $id | Numeric identifier of the record |
string $name | Name of the record to show to the user (if nothing else, #id) |
static string $_table | Name of the table where the records are stored |
__constructor() | Loads data from database (if argument is an id, otherwise argument fields) into the object |
load() | Loads data from database into the object |
save() | Saves data from object into database |
static find() | Look up records in database |
This is not a class, but a namespace for controllers. Each class in this namespace can have the following properties:
string $cli | Hint for CLI arguments |
string $mimetype | Change the default mime type of the output. Defults to 'text/html'. |
string $title | Controller specific page title. |
string $favicon | Controller specific favicon. |
array $meta | Additional meta tags for the controller. |
action() | Default action handler, see routing. |
HTTP protocol helpers.
None.
static url() | Generate permanent url |
static redirect() | Redirect user to url |
static get() | Perform a HTTP GET or POST request and return response |
static route() | Add routes to the routing table |
static urlMatch() | Query routing table and return class, method for an url |
DataSource services.
string $name | Name of primary datasource driver |
static db() | Add new database or return current datasource's PDO instance |
static ds() | Select datasource or return current selector |
static like() | Convert user input into an sql like phrase |
static exec() | Execute sql on current datasource |
static query() | Perform a SELECT query and return it's results |
static fetch() | Get the first record from database |
static field() | Get one field's value from database |
static tree() | Execute a reentrant SELECT to get a tree from database |
static bill() | Return time consumed by database operations |
Wrapper around cache implementations.
string $name | Current implementation (class under PHPPE\Cache namespace) |
static object $mc | Memcache (or any other implementation's) instance |
static set() | Store a value for key in cache |
static get() | Retrieve a value for key from cache |
This extension proxies assets under vendor directory. It also make use of cache if possible.
None.
static minify() | A very small and fast asset minifier |
The PHPPE templater.
None.
static assign() | Assign a variable to the templater |
static css() | Add stylesheet to the output |
static jslib() | Add JavaScript library to the output |
static js() | Add JavaScript function to the output |
static menu() | Add menu to PE Panel |
static template() | Get and parse a view template |
static get() | Get and return raw template without parsing it |
static getval() | Evaluate a templater expression |
static e() | Format an error message according to output |
static picture() | Crop, resize and watermark imagery |
Set of useful file manipulation tools.
None.
static rmdir() | Recursive directory remover |
static untar() | Extracts a file from an archive or execute a callback for each |
static ssh() | Executes shell command securely on a remote server |
static copy() | Copies files securely to a remote server |
static bg() | Execute a background job in every nth sec. |
Classes in PHPPE\Filter
namespace that are inherited from the PHPPE\Filter
class can be used as URL filters.
For that it has only one argumentless method that returns a boolean success flag.
You can apply filters to route calls as 4th argument, and they will be executed when the routing decision is made.
None.
static filter() | Logic that checks validity for an URL and returns boolean |
1
2
3
4
5
6
7
class \PHPPE\Filter\get extends \PHPPE\Filter { public static function filter() { return @$_SERVER[ 'REQUEST_METHOD' ] == "GET" ? true : false; } }
1
2
3
4
5
6
7
class \PHPPE\Filter\post extends \PHPPE\Filter { public static function filter() { return @$_SERVER[ 'REQUEST_METHOD' ] == "POST" ? true : false; } }
1
2
3
4
5
6
7
class \PHPPE\Filter\csrf extends \PHPPE\Filter { public static function filter() { return \PHPPE\Core::isBtn(); } }
1
2
3
4
5
6
7
8
9
10
class \PHPPE\Filter\loggedin extends \PHPPE\Filter { public static function filter() { if (\PHPPE\Core::$user->id) return true; /* save request uri for returning after successful login */ PHPPE\Http::redirect("login", true); } }
These are the building blocks of the view layer. They implement user input boxes, widgets and basicly everything that has to display formated values in a view.
To include an AddOn in a template, you use
<!tag addon(arguments) name attributes>Where tag can be one of
var
, field
, widget
or cms
. See templater for more details.
string $name | Name of the instance. Most likely something like "object.property" |
string $fld | Name of the instance, appropriate for DOM id or name. Most likely something like "object_property" |
string $value | Value of the instance. It's passed by reference in most cases. |
string $args | Arguments passed in pharenthesis after addon name. |
string $attrs | Attributes passed after instance name. |
string $css | Generated class name, "input", "reqinput" or "errinput". You can add more css classes with attributes if you like. |
string $conf | Configuration scheme. Used by Content Editor. |
init() | Initialization event handler. Called once for every referenced AddOn per page generation. |
show() | Uneditable html representation of the value. For fields is a formated value, for widgets it means the widget's face. |
edit() | Editable html representation of the value. For fields is more likely one or more INPUT fields, for widgets it's the configuration form. |
static validate() | Validates or alters the AddOn's value. |
This class is a bit tricky as PHPPE Core provides PHPPE\User (note singular) that can handle only one adminstrator user, whom password hash is set in configuration file. As soon as PHPPE Pack is installed under vendor/phppe/Core, that class will be transparently substituted with PHPPE\Users (note the prular) which can handle more users.
This class is a classic Model, it's inherited from PHPPE\Model therefore you can use the find() method to list users.
int $id | Numeric identifier of the user |
string $name | Login name |
string $email | Email address of the user |
int $parentid | The parent user's id. Users can be organized in a tree. |
boolean $active | True if user is allowed to log in |
array $data | User preferences in an associative array |
has() | Checks whether the given user has an ACE |
grant() | Grant ACE to the user |
clear() | Terminate access |
login() | Authenticate user with login name and password |
logout() | Log out user |
In most cases configuration file is enough for extensions. But sometimes extensions need to store a value as well, e.g.: timestamp of the lust run. Because PHPPE does not allow the webserver's user to write php files, it cannot change config.php, so it would need a different mechanism, and that's what Registry is for. Without a datasource, it will use local files to store changable configuration variables, uses 'registry' table otherwise.
None.
static set() | Store a value for key in registry |
static get() | Retrieve a value for key from registry |
static del() | Remove a key from registry |
SQL Query Builder, built on top of datasource.
None.
static like() | Convert user input into an sql like phrase |
static select() | Start to build a SELECT query |
static update() | Start to build an UPDATE statement |
static insert() | Start to build an INSERT statement |
static replace() | Start to build a REPLACE statement |
static delete() | Start to build a DELETE statement |
static truncate() | Start a TRUNCATE statement |
table() | Add plus table to the query with table multiplication |
join() | Join a table to the query |
fields() | Specify fields to query |
where() | Add where clause |
orderBy() | Sort records |
groupBy() | Group records |
having() | Same as where clause, but filters aggregated records |
limit() | Maximum number of records to return |
offset() | Number of records to skip at begining |
sql(), __toString() | Return SQL sentence string |
execute() | Execute the query/statement on current datasource |
with() | Execute the query/statement with a value set |
Throws DBException
.
Email builder and sender. Supports multipart messages and can speak SMTP directly (no dependency to third lib classes).
The backend is configured in phppe/config.php.
smtp://[(user)[:(pass)@]](server)[:(port)][/(sender email address)] | This is the default backend, speaks SMTP natively, no dependencies required. |
mime | This backend builds and returns the mime message but does not send any email. |
log | This backend does not send any email either, it's just writes the log, for testing purposes. |
Uses PHP's built-in mail() function. Not recommended. | |
sendmail | Sends mails by calling sendmail through a pipe. Not recommended. |
phpmailer | Uses the well-known PHPMailer class to send out the emails. |
db | Stores emails in database. In this case the cron minute background job will send out the mail with the backend configured in "realmailer". |
None.
__constructor() | Create an Email object |
get() | Returns Email object in JSON |
to() | Set recipient |
replyTo() | Set return address |
cc() | Set carbon copy address |
bcc() | Set hidden carbon copy address |
subject() | Set subject |
message() | Set message body using string |
template() | Set message body using a view template |
attachFile() | Add attachment from file |
attachData() | Add attachment from a string |
send() | Send the email |
Throws EmailException
.
Allows manipulation of Raspberry Pi GPIO ports from PHP, and also queries some useful property of the board.
In order to get GPIO working, you'll have to add the webserver's user to the gpio group:
# addgroup www-data gpio
Also, you should add /sys/class/gpio
and /proc/cpuinfo
to open_basedir in your php.ini file.
array $pins | Pin layout |
static RPiPCB() | Returns board revision number (for pin autodetection) |
static temp() | Returns board temperature in SI oC |
static freq() | Returns CPU frequency |
static reset() | Reset all pins to input mode |
static mode() | Set direction (in or out mode) for a pin |
static get() | Read level of a pin |
static set() | Set high or low level on a pin |
Throws GPIOException
.
Adds functionality to PHPPE JavaScript libraries.
htmlspecialchars(text) | Escape a string for html attributes, like in php. |
urlencode(text) | Escape a string for urls, like in php. |
number_format(number, decimals, dec_point, thousands_sep) | Format a number, like in php. |
function_exists(functionname) | Checks if a function exists. Name can contain namespace. |
getpos(domElement) | Returns the position and dimension of an element in {"x","y","w","h"}. |
L(phrase) | Translate a phrase |
pe.cookie.get(name) | Read the value of a cookie. |
pe.cookie.set(name,value,nDays,path) | Store a cookie. |
pe.store.get(name) | Read the value of a store item. |
pe.store.set(name,value) | Store an item. Passing null as value will remove the item. |
pe.dnd.drag(event,id,icon,size,endhook) | Drag a DOM element represented as icon with size. |
pe.dnd.drop(event,myfunc[,context]) | Mark a DOM element as drop area. |
myfunc(what,context,event) | Callback arguments for both myfunc and endhooks. |
You can also use the data attribute version, add data-drag="id,icon,size,endhook"
to draggable elements,
and data-drop="callback[,context]"
to drop areas.
pe.popup.open(triggerobj,popupid,deltax,deltay) | Pops up the div with id of 'popupid'. |
pe.popup.close() | Hide the active pop up div. |
Or use data-popup="popupid"
on the trigger elements.
To make your table responsive, add "resptable"
class to it.
pe.setsel.search(id) | Execute list filtering on a widget. |
An intuitive multiple selection widget.
pe.user.profile(this) | Pops up user profile page with preferences. |
pe.cookiealert.init({ "cookiename", "message", "moreurl", "morelabel", "acceptlabel" }) | Warning on cookies with a more button. |
pe.wyswyg.open(this,icons) | Turns a textarea into a fully featured editor. |
Alternatively add "wyswyg"
class to your textarea element.
public run( string $app, string $action );
Execute a PHPPE Application.
string $app | Override application |
string $action | Override action |
None.
1
2
$phppe = include("public/index.php"); $phppe->run();
public static lib( ); public static lib( string $name ); public static lib( string $name, string/object $service, string/array $dependencies );
This method is for dealing with libraries (services). Calling without argument or with exactly one argument is a query, applying more arguments define a new service.
Normally you'll never ever need to register a service. You should instead return an instance from your extension's init.php.
string $name | Name of the library / service |
string/object $service | Name of the class (if it's static) or instance reference |
string/array $dependencies | Dependencies, comma separated string list or array |
array of instances | When called without arguments, returns all service instances |
instance or null | When called with a service name, returns instance for it |
null | When called with at least two arguments, registers new service |
1
2
3
4
5
6
7
8
//! query installed services $all = Core::lib(); //! query a service instance $gpio = Core::lib("GPIO"); //! add a new service with static class. Normally you don't need to do that Core::lib("myService", "\PHPPE\myService"); //! add a new service with instance and dependency check. Normally you don't need to do that Core::lib("myService", $myService, "Users,Email");
public static isInst( string $name );
Installation check, returns true if service or AddOn exists.
string $name | Name of the library / service / AddOn |
boolean | True if installed |
1
2
//! check if a service is installed $hasGpio = Core::isInst("GPIO");
public static lang( string $name );
Loads a language dictionary into memory for a class (extension).
string $name | Name of the class / extension |
None.
1
2
//! load a language dictionary Core::lang("GPIO");
L( string $phrase, ... );
Translate a text to user's language. This function is available almost everywhere: in PHP, in JavaScript and in view templates as well.
The dictionary has to be loaded with Core::lang().
You can pass arguments to the function if the translated text contains %s or %d. Note that JS version just substitutes strings, it does not format them.
string $phrase | Phrase to translate |
mixed ... | Variable number of arguments if phrase contains %s or %d. |
string | Translated text. |
1
2
3
4
5
6
//! translate a keyword L("myextension_mylabel"); //! translate direct text L("Thank You!"); //! translate with arguments L("%s logged in", $username);
public static log( string $weight, string $message, string $facility );
Logger. Don't forget that your messages most probably will be parsed by scripts, so use English messages if possible without translation.
When configured to log to syslog, no files will be written, and the message will be sent as:
(vhost name):PHPPE-(A|W|E|C|I|D)-(facility):(original message without line breaks)
It's easy to grep. If you also turn on trace, then the caller's filename and line will be logged as well.
data/log
string $weight | Can be one of: A - Audit (will be logged regardless to runlevel) D - Debug (only logged at runlevel 3) I - informational W - Warning (some features will be disabled) E - Error (some important part failed, but page can be still generated) C - Critical (main part failed, the page cannot be generated) NOTE: logging Critical message will stop PHP script execution.
|
string $message | The message to log. Put in as much useful information as you can. |
string $facility | Which logfile to use. If not given, it will be autodetected after the extension name calling |
None.
1
Core::log("D", "I'm running!", "myService");
public static isBtn( string $name );
Checks if the user is trying to save a form by pressing a button. This includes CSRF check as well.
string $name | Name of form to check, if not given, the first form is checked. |
int | The serial number of the button pressed on the form, starts with 1. Zero means no button was pressed, or CSRF check failed. |
1
2
3
4
5
6
7
//! typical in action handlers if (Core::isBtn("myform")) { $myform = Core::req2obj("myform"); ... } //! save which button was pressed $btnNo = Core::isBtn("userform");
public static error( ); public static error( string $message, string $field );
When called without arguments, returns error messages, otherwise adds a new error to the page or to a field.
<!include errorbox>
string $message | The error message |
string $field | If the message is related to a field, it's name |
array of messages | When called without arguments, returns all messages |
null | When called with arguments, registers new message |
1
2
3
4
5
6
//! query error messages $errors = Core::error(); //! set an error message Core::error("Something went wrong!"); //! set an error for a field Core::error("Please fill out this field.", "myform.username");
public static isError( string $field );
Check if there was an error.
string $field | If you are interested in a specific field, it's name |
boolean | True if there was an error |
1
2
3
4
//! is there any error message? $wasError = Core::isError(); //! is there a problem with a field? $badInput = Core::isError("myform.username");
public static event( string $type, array $context );
Triggers an event, and executes all event hook for it. You don't have to use this method, as events are triggered by Core.
string $event | Event type |
array $context | Optional context. Each event type has different context. |
array | Optionally altered context |
1
2
//! trigger event list($app, $action) = Core::event("route", [$app, $action]);
public static validate( string $name, string $validator, boolean $required, array $arguments, array $attributes );
Adds extra validation rule to a field. Note that type of the field automaticaly adds validation.
Validator will be prefixed by "PHPPE\AddOn" and the resulting class' validate() method will be used.
string $name | Field name, like "myform.username". |
string $validator | AddOn name that has the wanted validation |
boolean $required | True if value cannot be empty |
array $arguments | Arguments to the AddOn |
array $attributes | Attributes to the AddOn |
None.
1
2
//! add extra validation to a field Core::validate("myform.email", "filtermx");
public static req2arr( string $form, array $rules ); public static req2obj( string $form, array $rules );
Get user input from dragon land with validation and return it in an associative array (or stdClass). Usually you don't have to worry about validation rules as PHPPE\View::template() will build the appropriate $rules array according to the fields used in the view.
string $form | Name of the form, $_REQUEST key prefix. |
array $rules | Additional validation rules. |
array | Optionally altered context |
1
2
3
4
5
//! typical in action handlers if (Core::isBtn("myform")) { $myform = Core::req2arr("myform"); ... }
public static arr2str( array $subject, string/array $exclude, string $separator ); public static obj2str( object $subject, string/array $exclude, string $separator );
Convert an associative array or object into a properly escaped string, appropriate for XML attributes or SQL queries.
array/object $subject | What you want to convert |
string/array $exclude | Comma separated list or array of keys to exclude. |
string $separator | String to use between key=value pairs. Defaults to space. |
string | Properly assembled string with escaped values. |
1
2
3
4
5
6
7
8
9
10
11
12
13
/* $obj => stdClass Object ( [Id] => 123 [Name] => Something [AnotherField] => good [Flag] => 1 ) */ //! for SQL Core::obj2str( $obj ) => "Id='123',Name='Something',AnotherField='good',Flag='1'" Core::arr2str( $obj, "Id,Flag" ) => "Name='Something',AnotherField='good'" //! for XML attributes Core::obj2str( $obj, "", " ") => "Id='123' Name='Something' AnotherField='good' Flag='1'"
public static val2arr( string $expression, string $separator );
Get the value of a templater expression and convert it into an array.
string $expression | A templater expression |
string $separator | String to use to split if expression evaluates to a string. Defaults to comma. |
array | Returns an array. |
1
2
3
4
5
//! $obj->field = [ "a", "b", "c" ] $arr = Core::val2arr( "obj.field" ); //! $obj['field'] = "a;b;c" $arr = Core::val2arr( "obj.field", ";" ); //! $arr = [ "a", "b", "c" ] in both cases
public static tre2arr( array $subject, string $prefix ); public static tre2arr( array $subject, string $prefix, string $suffix );
Flat a tree stored in an array into a one-level array. It has two operation modes:
1. without suffix it generates arrays for SELECT boxes.
2. with suffix given, it generates arrays for XML trees.
See examples below.
array $subject | Tree array, children arrays in "_" as returned by PHPPE\DS::tree() |
string $prefix | Prefix string to represent recursion level. In select box mode it will be repeated, for XML trees it has to have a '%d' that will be substituted. |
string $suffix | Suffix for XML mode |
array | Returns a flat array. |
1
2
3
4
5
//! option lists for select boxes $arr1 = Core::tre2arr( $tree, " " ); //! XML mode, for AJAX loaded category trees $arr2 = Core::tre2arr( $tree, "<div data-level=%d>", "</div>" ); //! note the differences in the 'name' item below:
Array ( [0] => stdClass Object ( [id] => 1 [name] => item1 ) [1] => stdClass Object ( [id] => 2 [name] => item2 [_] => Array ( [0] => stdClass Object ( [id] => 21 [name] => item2.1 [_] => Array ( [0] => stdClass Object ( [id] => 211 [name] => item2.1.1 ) ) ) [1] => stdClass Object ( [id] => 22 [name] => item2.2 ) [2] => stdClass Object ( [id] => 23 [name] => item2.3 ) ) ) [2] => stdClass Object ( [id] => 3 [name] => item2 ) )
Array ( [0] => stdClass Object ( [id] => 1 [name] => item1 ) [1] => stdClass Object ( [id] => 2 [name] => item2 ) [2] => stdClass Object ( [id] => 21 [name] => item2.1 ) [3] => stdClass Object ( [id] => 211 [name] => item2.1.1 ) [4] => stdClass Object ( [id] => 22 [name] => item2.2 ) [5] => stdClass Object ( [id] => 23 [name] => item2.3 ) [6] => stdClass Object ( [id] => 3 [name] => item2 ) )
Array ( [0] => stdClass Object ( [id] => 1 [name] => item1 ) [1] => stdClass Object ( [id] => 2 [name] => item2 <div data-level=1> ) [2] => stdClass Object ( [id] => 21 [name] => item2.1 <div data-level=2> ) [3] => stdClass Object ( [id] => 211 [name] => item2.1.1 </div> ) [4] => stdClass Object ( [id] => 22 [name] => item2.2 ) [5] => stdClass Object ( [id] => 23 [name] => item2.3 </div> ) [6] => stdClass Object ( [id] => 3 [name] => item2 ) )
public static bm( string $name );
Sets a benchmark point in code. Every point is measured relative to it's accessor point.
string $name | Unique name. |
None.
1
2
3
// ...do time consuming calculation here... //! mark a spot in code Core::bm("calculation");
public static has( string $class, string $method );
Checks if a class can be autoloaded or if it has a method.
string $class | Class name |
string $method | Method name |
boolean | True is class can be autoloaded (and if it has a method) |
1
2
3
4
//! check if there's a class ClassMap::has("Extension"); //! check if there's a class and also it has a given method ClassMap::has("Extension", "init");
public static ace( );
Return valid Access Control Entries.
None.
array | List of valid ACEs |
1
$ace = ClassMap::ace();
public static map( );
Return classes along with the files they were defined in.
None.
array | Classes as keys and files as values |
1
$map = ClassMap::map();
new Model( int/string $id ); new Model( mixed $properties );
Loads object properties from the argument (if it's an array or object) or from database (if argument is an id).
int/string $id | Identifier |
mixed $properties | Associative array or object, initial properties |
mixed | A Model instance |
1
2
3
4
//! load a model instance by id $user= new Users(1); //! load by arguments $user= new Users(["email"=>"some@where.tld"]);
public load( int $id ); public load( string $searchphrase, string $where, string $order );
Loads object properties from the first database record found.
int $id | Identifier |
string $searchphrase | String to search for |
string $where | Where clause |
string $order | When using search, defines the ordering |
boolean | True on success |
1
2
3
4
//! load a model instance by id $user->load(1); //! load by searching for a record $user->load("me@localhost", "email=?", "created DESC");
public save( boolean $new );
Saves a Model object into database. If the model has no identifier, it will be INSERTed, otherwise UPDATEd. With flag given it will INSERT the object with the given id.
boolean $new | Force INSERT of a new object with id |
boolean | True on success, the $obj->id porterty also updated on INSERT |
1
2
3
4
5
//! save a model, INSERT or UPDATE depending on whether it has id $user->save(); //! save as a new object with INSERT, despite it has an id and should be an UPDATE $user->id = 1; $user->save(true);
public find( string $searchphrase, string $where, string $order, string $fields );
Finds instances of a model in database.
string $searchphrase | String to search for |
string $where | Where clause, defaults to "id=?" |
string $order | When using search, defines the ordering |
string $fields | Which columns to return, defaults to all |
array | Results |
1
2
3
4
//! get all users with all fields $users = Users::find(); //! get email address for all users named Joe $users = Users::find( DS::like("Joe"), "name like ?", "created DESC", "name,email");
public static url( string $app, string $action );
Generate a permanent url for the current (or other) application and it's action. It will use Core properties to do that, namely $core->sec and $core->base.
string $app | Override application |
string $action | Override action |
string | URL |
1
2
url(); // => "http://localhost/myapp/myaction" url("users", "add"); // => "http://localhost/users/add"
public static redirect( string $url, boolean $save );
Redirects user. Without argument, to the current page or to the saved URL.
string $url | URL to redirect to. |
boolean $save | Save the current URL for later redirection |
None.
1
2
3
4
5
6
7
8
9
10
//! these two are identical when there's no saved URL Http::redirect(); Http::redirect(Core::$core->app."/".Core::$core->action); //! redirect to the home page Http::redirect("/"); //! if user is not logged in, save the current URL and redirect to login page if (!Core::$user->id) Http::redirect("login", true); //! redirect to another site Http::redirect("https://netbank.com/payment?return=".urlencode(url())."&shopid=1234");
public static get( string $url, array $post, int $timeout );
Get content by making a HTTP request. This is better than file_get_contents() and CURL, because it handles redirections and keep track of cookie changes as well. Also it automatically passes the user's preferred language.
string $url | URL to get. |
array $post | Post parameters in an associative array |
int $timeout | Timeout in seconds, defaults to 3 |
string | Contents of the HTTP response, without header. |
1
2
3
4
//! make a GET request $page = Http::get("https://webservice.org/api"); //! make a POST request $page = Http::get("https://webservice.org/api", ["apikey"=>"1234", "func"=>"list"]);
public static route( string $urlmask, string $class, string $method, string/array $filters); public static route( stdClass( url => $urlmask, name => $class, action => $method, filters => $filters ) ); public static route( [ "url" => $urlmask, "name" => $class, "action" => $method, "filters" => $filters ] ); public static route( [ $urlmask => [ $class, $method, $filters ] ] ); public static route( [ $urlmask => [ $class, $method, $filters ], ... ] );
Add new URL route to the routing table. Call it from init.php or from extension init() method.
string $urlmask | Regular expression to catch URL. If it has pharenthesis, matched pattern will be passed to action handler. |
string $class | Controller class to handle the matched URL. |
string $method | Controller method (action handler) to handle the URL. |
string/array $filters | Comma separated list of filters or ACEs checks on the route |
None.
See URL routing.
public static urlMatch( string $app, string $action, string $url );
Returns controller and action handler name for current (or given) URL.
string $app | Default controller class (if no routes match) |
string $action | Default controller method (if no routes match) |
string $url | URL to match, defaults to current URL |
array [ string $class, string $method, array $arguments ] |
Controller according to matched URL. If there was a match, but failed due to a filter, $class will be set to "403 " |
1
list($app, $action, $args = Http::urlMatch("Content", "action", "url/to/match");
public static db( ); public static db( string $pdodsn, object $instance );
When called without arguments, returns the current PDO instance. Otherwise connects a new database as datasource.
string $pdodsn | PDO Data Source Name. Note: if otherwise not supported, you can append "@username:password" |
object $instance | Any PDO compatible object associated with the dsn |
PDO/null | PDO instance for current selector when called without arguments or null |
int | Newly allocated data source's selector |
1
2
3
4
5
6
7
//! query current PDO instance $pdo = DS::db(); //! connect new PDO instances $selector = DS::db("sqlite:data/db.sqlite"); $selector = DS::db("mysql:host=localhost;dbname=db;charset=utf8@user:pass"); //! connect an arbitrary, but PDO compatible class $selector = DS::db("salesforce:", $salesforce);
public static ds( ); public static ds( int $selector );
When called without arguments, returns the current datasource selector. With argument given selects a new datasource.
int $selector | Datasource selector (as returned by db() call) |
int | Current (or newly selected) data source's selector |
1
2
3
4
//! query current data source $selector = DS::ds(); //! select a new data source $selector = DS::ds(2);
public static like( string $str );
Converts a user provided string into an sql-safe, search ready like phrase.
string $str | User provided string |
string | Sql-safe like phrase |
1
2
DS::like("search this"); // => "%search%this%" DB::like("search this"); // => "%search%this%"
public static exec( string $sql, array $args );
Executes an sql query on current datasource.
string $sql | Sql query or sentance with '?' placeholders |
array $args | Values for the placeholders |
array | For SELECT queries, result set in array of associative array records |
int | Number of affected rows for all other sentances |
1
2
3
$results = DS::exec("SELECT * FROM users"); $rowno = DS::exec("DELETE FROM users WHERE name like ?", [ "Joe" ]); $rowno = DS::exec("INSERT INTO users SET ".Core::obj2str(Core::$user));
public static query(string/array $fields, string $table, string $where, string $groupBy, string $orderBy, int $offset, int $limit, array $args );
Executes an sql SELECT query on current datasource. This is a low level method, you should use PHPPE\DB instead.
string/array $fields | Comma separated list or array |
string $table | Table or joins |
string $where | Where clause |
string $groupBy | Group by fields |
string $orderBy | Order by fields |
int $offset | Number of records to skip at the begining |
int $limit | Maximum number of records to return |
array $args | Values for the placeholders |
array | Result set in array of associative array records |
1
2
3
4
5
6
7
8
9
10
$results = DS::query( "a.*,b.topic", "forum_post a LEFT JOIN forum_topic b ON a.topicId=b.id", "a.userId=?", "b.id", "a.userId DESC", 0, 100, [ Core::$user ] );
public static fetch(string/array $fields, string $table, string $where, string $groupBy, string $orderBy, array $args );
Executes an sql SELECT query on current datasource and returns the first record. This is a low level method, you should use PHPPE\DB instead.
string/array $fields | Comma separated list or array |
string $table | Table or joins |
string $where | Where clause |
string $groupBy | Group by fields |
string $orderBy | Order by fields |
array $args | Values for the placeholders |
stdClass | Object record |
1
2
3
4
5
6
7
8
$record = (array)DS::fetch( "a.*,b.topic", "forum_post a LEFT JOIN forum_topic b ON a.topicId=b.id", "a.userId=?", "b.id", "a.userId DESC", [ Core::$user ] );
public static field(string/array $fields, string $table, string $where, string $groupBy, string $orderBy, array $args );
Executes an sql SELECT query on current datasource and returns the first column of the first record. This is a low level method, you should use PHPPE\DB instead.
string/array $fields | Comma separated list or array |
string $table | Table or joins |
string $where | Where clause |
string $groupBy | Group by fields |
string $orderBy | Order by fields |
array $args | Values for the placeholders |
string | Value of the first column of the first record |
1
$time = DS::field("CURRENT_TIMESTAMP");
public static tree( string $sql, int $parentId );
Executes several sql queries on current datasource to return a tree (or sub-tree). You can use Core::tre2arr() to convert the result into an option list or XML tree.
string $sql | Sql query exactly one '?' placeholder to mark the place of parent id |
int $parentId | Parent Id for which the sub-tree is queried. 0 means return the whole tree |
recursive array | Array of associative array records, child branches in "_" |
1
$subTree = DS::tree("SELECT id,parentId,name FROM category WHERE id!=parentId AND parentId=?", 2);
public static bill( );
Returns the time consumed by database queries in second with microsec precision.
None.
float | Time consumed by database queries in second |
1
2
3
4
$start = DS::bill(); //! do some database stuff here DS::exec("DELETE FROM users"); $benchmark = DS::bill() - $start;
public static set(string $key, mixed $value, int $ttl, boolean $force );
Saves a value to cache for key.
string $key | Key for reference |
mixed $value | Value to be stored |
int $ttl | Time to live in cache |
boolean $force | Save the value in cache even if Core::$core->nocache is set |
boolean | True on success |
1
Cache::set("rssnews", $rssNews, 60);
public static get(string $key, boolean $force );
Retrive a value from cache for key.
string $key | Key for reference |
boolean $force | Look up cache even if Core::$core->nocache is set |
mixed | Stored value or null on cache miss |
1
$rssNews = Cache::set("rssnews");
public static minify( string $subject, string $type );
Compress code by removing whitespace characters. If installed, it will use CSSMin and JSMin. Minifying can be turned off by setting Core::$core->nominify in configuration.
string $subject | String to compress |
string $type | Kind, one of 'css ', 'js ' or 'php ' |
string | Minified subject. If type unrecodnized, the original string |
1
2
3
4
$compressed = Assets::minify(" .mystyle1 { font-size: 16px; }", "css");
public static assign( string $name, mixed $value );
Assign variable to templater.
string $name | Name of the variable |
mixed $value | Value to assign |
None.
1
2
3
View::assign("myModel", $myModel); //! then reference in templates as: //! <!=myModel.name>
public static css( ); public static css( string $file );
When called without parameters, returns assigned stylesheets, add a new css to templater otherwise.
string $file | Name of the stylesheet to add |
array | When called without parameters, returns assigned stylesheets |
1
2
$css = View::css(); View::css("mystyle.css");
public static jslib( ); public static jslib( string $file, string $initcode, int $priority );
When called without parameters, returns assigned JavaScript libraries, add a new library to templater otherwise.
string $file | Name of the library to add |
string $initcode | Code to be executed on init event to initialize the library |
int $priority | Define in which order to be added. 0 = JavaScript framework libraries like jQuery 1-9 = JavaScript basic libraries like bootstrap >9 = Normal JavaScript libraries |
array | When called without parameters, returns assigned libraries |
1
2
$jslibs = View::jslib(); View::jslib("mylib.js", "mylib.init()");
public static js( string $function, string $code, boolean $append );
Adds a JavaScript function to output.
string $file | Name of the function |
string $code | JavaScript code. It will be minified automatically |
boolean $append | If true, the code will be added to existing function code |
None.
1
2
View::js("myfunc(name)", "alert('Hello '+name+'!');"); View::js("init()", "alert('Hello World!');", true);
public static template( string $template, array $variables );
Load, parse and evaluate a template.
string $template | Name of the template |
array $variables | Additional view variables (name=>value) to assign |
string | Parsed template |
1
$html = View::template("index");
public static get( string $template );
Load and return template as is, without parsing.
string $template | Name of the template |
string | Raw, unparsed template, from cache is available |
1
$tpl = View::get("index");
public static getval( string $expression );
Evaluate a templater expression and return the result. Used by <!=...>
tag.
In nested layout iterations, you can refer to fields of outter iteration with prefix parent.
.
What's more, you can use special variables:
IDX
- iteration counter
ODD
- true if iteration counter is odd
KEY
- the key of the array you're iteration on
VALUE
- the value of the array element you are currently iteration on
parent.IDX
to get the index counter of the outer foreach.string $expression | Templater expression |
mixed | Evaluated expression |
1
$libs = View::getval("core.libs()");
In view templates:
<!=core.libs()> <!=sprintf("%s / %s", parent.name, name)>
public static e( string $weight, string/array $message, string $facility );
Properly format an error message, depending on Core::$core->output.
string $weight | One of A,W,E,C,I,D. See Core::log() |
string/array $message | The error message or more messages |
string $facility | The facility or extension that the error is generated for |
string | Formatted message |
1
echo( View::e("E", L("File not found"), "myService") );
public static picture( string $source, string $target, int $maxWidth, int $maxHeight, boolean $crop, boolean $lossless, string $watermark, int $maxSize, int $minQuality );
Resize, crop and watermark image. Will reduce file size by decreasing quality to the given limit.
When crop, it will resize the image to the width or to the height which is smaller, then cut along the larger one. The resulting image's dimensions will be exactly maxWidth x maxHeight.
In resize mode it will resize the image to fit in maxWidht x maxHeight, but keeping original picture's aspect ratio, therefore the resulting image could be smaller in dimensions than maxWidth x maxHeight.
string $source | The image's path and filename you want to resize/crop. |
string $target | Destination path and filename. |
int $maxWidth | Maximum width of the new image. |
int $maxHeight | Maximum height of the new image. |
boolean $crop | If true, it will crop the image, otherwise it resizes keeping aspect ratio. Defaults to false (resize) |
boolean $lossless | Whether to use lossless compression (png) on resuting image. If false, it will save jpeg. Defaults to true (png) |
string $watermark | Path and filename of watermark logo. If given it has to be a semi-transparent png with alpha channel. |
int $maxSize | Maximum file size of the resulting image in kilobytes. If it's bigger, picture() will reduce it's quality until file size or minimum quality requirements are met. Defaults to 8192 (8MiB) |
int $minQuality | Minimum image quality when reducing file size (1-10). Defaults to 5 (50%). |
boolean | True on success. On failure the original image will be simply copied as target. |
1
2
3
4
5
$f = $_FILES["myFile"]; //! creating fullscreen jpeg image with watermark View::picture($f["tmp_name"], "data/large/".$f["name"], 1024, 768, false, false, "data/companylogo.png" ); //! creating small (64k) thumbnail for it as well View::picture($f["tmp_name"], "data/thumb/".$f["name"], 128, 128, true, true, "", 64 );
public static rmdir( string $directory );
Recursively remove a directory with all of it's contents.
string $directory | Directory to remove |
boolean | True on success |
1
Tools::rmdir("data/cache");
public static untar( string $archive, string $filename ); public static untar( string $archive, callback [ string $class, string $method ] );
Useful tool to deal with archive files. Autodetects file format and can handle PKZIP, tar, tar gz, tar bz2, cpio, cpio gz, cpio bz2 and pax.
When second argument is a string, it will look for the file in the archive and returns it's contents.
When second argument is an array with a class + method pair, it will call it for every file in the archive.
string $archive | Path and file of the archive |
string $filename | Name of the file to extract |
callback [ $class, $method ] | Callback method that will be called on every file in the archive. |
string $filename | Name of the file |
string $content | Uncompressed content of the file |
string | Contents of the file or null |
1
2
3
4
5
6
$json = Tools::untar("phppe3_core.tgz", "composer.json"); Tools::untar("sitebuild.zip", ["myClass", "saveFile"]); //! and the callback in the class myClass function saveFile($filename, $content) { file_put_contents(".tmp/extracted/".$filename, $content); }
public static ssh( string $command, string $input, string $localCommand ); public static ssh( "rsync", array $dirs, string $remoteDir );
Execute a command on a remote server that's defined in current user's preferences in Core::$user->data['remote']. For details, see Extensions.
Special case when command is "rsync". In that case it will synchronise directories to a remote site. If you want to push all files, use PHPPE\Tools::copy().
string $command | Command to execute remotely |
string $input / $dirs | If given, it will send to remote command's stdin. If command is "rsync", then an array of filenames to sync. |
string $localCommand / $remoteDir | If given, it will be executed locally and it's output will be piped to the remote command's stdin through SSH tunnel. With "rsync", the directory to sync to on remote host. |
string | Output of the remote command |
1
2
3
4
5
6
7
8
//! execute a command remotely and get it's output $files = Tools::ssh("ls -la"); //! save a file remotely Tools::ssh("cat >composer.json", file_get_contents("composer.json") ); //! pipe local command's standard output to remote command's standard input Tools::ssh("cat >installedby.txt", "", "whoami"); //! synchronize directories securely Tools::ssh("rsync", [ "/usr/git/myRepo", "/var/www" ], "/var/production/www");
public static copy( string/array $files, string $destination );
Copy files to a remote server that's defined in current user's preferences in Core::$user->data['remote']. For details, see Extensions.
With copy you can push whole directories to the remote site. If you want to lower the load, use PHPPE\Tools::ssh('rsync').
string/array $files | Path and filename of the files to copy |
string $destination | Destination directory on remote server. |
string | Empty string if everything went well, error message otherwise |
1
2
3
4
//! copy a file to remote server $error = Tools::copy("composer.json"); //! copy more files to remote server to a specific directory $error = Tools::copy(["data/something", "data/sqlite.db"], "data/incoming");
public static bg( string $className, string $methodName, mixed $argument, integer $interval );
Execute a background job in every nth second using webserver's rights.
string $className | Name of the class to call |
string $methodName | Name of the method to call |
mixed $argument | Any arbitrary data that is passed to the method. |
integer $interval | Running interval in secs. |
None.
1
2
//! call $myClass->myMethod('cleanup','me') every 5 secs. \PHPPE\Tools::bg( "myClass", "myMethod", [ "cleanup", "me" ], 5 /*secs*/ );
final public has( string/array $ace );
Checks if the user has the given Access Control Entry.
string/array $ace | Pipe separated list or array of Access Control Entries |
boolean | True if user has the access |
1
$loggedin = Core::$user->has("loggedin");
final public grant( string/array $ace );
Grant access to the user.
string/array $ace | Pipe separated list or array of Access Control Entries |
None.
1
Core::$user->grant("webadm|siteadm");
final public clear( string/array $ace );
Revoke access from the user.
string/array $ace | Pipe separated list or array of Access Control Entries |
None.
1
Core::$user->clear("webadm|siteadm");
public login( string $username, string $password );
Authenticate user.
string $username | Name of the user |
string $password | Password of the user |
boolean | True if authentication was successful |
1
2
3
if (Core::$user->login("admin", "changeme") ) { //! user logged in successfully }
public logout( );
Log out user.
None.
1
Core::$user->logout();
public static set( string $key, mixed $value );
Sets a value for key in registry.
string $key | Key to use |
mixed $value | Value to store |
boolean | True if value stored successfully |
1
Registry::set("myService_lastRun", time() );
public static get( string $key, mixed $default );
Get a value for key from registry.
string $key | Key for which to get value |
mixed $default | Default value if key not found |
mixed | Value or null |
1
$lastrun = Registry::get("myService_lastRun", 0);
public static del( string $key );
Remove a key and it's value from registry.
string $key | Key to delete |
None.
1
Registry::del("myService_lastRun");
public static select( string $table, string $alias );
Create a SELECT query sentance.
string $table | Name of the table to work with |
string $alias | Alias for the table |
DB | SQL sentance object |
1
2
3
$result = DB::select("forum_topic", "f") ->fields("f.*") ->execute();
public static update( string $table, string $alias );
Create an UPDATE sentance.
string $table | Name of the table to work with |
string $alias | Alias for the table |
DB | SQL sentance object |
1
2
3
$result = DB::update("forum_topic", "f") ->fields( ["f.id", "f.title"]) ->execute([ 3, "New title"]);
public static insert( string $table, string $alias );
Create an INSERT INTO sentance.
string $table | Name of the table to work with |
string $alias | Alias for the table |
DB | SQL sentance object |
1
2
3
$result = DB::insert("forum_topic", "f") ->fields( ["f.id", "f.title"]) ->execute([ 3, "New title"]);
public static replace( string $table, string $alias );
Create a REPLACE INTO sentance.
string $table | Name of the table to work with |
string $alias | Alias for the table |
DB | SQL sentance object |
1
2
3
$result = DB::replace("forum_topic", "f") ->fields( ["f.id", "f.title"]) ->execute([ 3, "New title"]);
public static delete( string $table, string $alias );
Create a DELETE sentance.
string $table | Name of the table to work with |
string $alias | Alias for the table |
DB | SQL sentance object |
1
2
3
$result = DB::delete("forum_topic") ->where( [["id", "=", 3]]) ->execute();
public static truncate( string $table );
Create a TRUNCATE sentance.
string $table | Name of the table to work with |
string $alias | Alias for the table |
DB | SQL sentance object |
1
2
$result = DB::truncate("forum_topic") ->execute();
public table( string $table, string $alias );
Add a new table multiplication to a sentance.
string $table | Name of the table to work with |
string $alias | Alias for the table |
DB | SQL sentance object |
1
2
3
$result = DB::select("forum_topic", "ft") ->table( "forum_posts", "fp") ->execute();
public join( string $type, string $table, string $condition, string $alias );
Add a new table by joining it to a sentance.
string $type | One of 'INNER', 'CROSS', 'LEFT', 'RIGHT'.'OUTER', 'NATURAL LEFT', 'NATURAL RIGHT', 'NATURAL LEFT OUTER', 'NATURAL RIGHT OUTER' |
string $table | Name of the table to work with |
string $condition | Join condition |
string $alias | Alias for the table |
DB | SQL sentance object |
1
2
3
$result = DB::select("forum_posts", "fp") ->join( "LEFT", "forum_topic", "fp.topicId=ft.id", "ft") ->execute();
public fields( string/array $fields );
Add fields to a sentance. If you have field names in array keys, you'll need with().
string/fields $fields | Comma separated list or array of fields to add. Their values should be passed as arguments with execute(). |
DB | SQL sentance object |
1
2
3
4
5
6
7
$result = DB::update("forum_topic", "f") ->fields( ["f.id", "f.title"]) ->execute([ 3, "New title"]); // note the with() method $result = DB::update("forum_topic", "f")->with( [ "f.id" => 3, "f.title" => "New title" ]);
public where( string/array $wheres, string $operator );
Add WHERE clause(s) to a sentance. If you want to filter after grouping, you should use having().
string/array $wheres | Where clase expression(s) to add |
string $operator | Operator, one of 'AND' or 'OR'. Defaults to 'AND' |
DB | SQL sentance object |
string | Simpliest form, like 'id=?' |
array of strings | More simple form clauses concatenated by the operator, like [ 'id=?', 'name=?' ] |
array of arrays | Separated form clauses concatenated by operator, like [ ['id', '=', '?'], ['name', '=', $value] ] Here the 2nd item can be one of: '=', '!=', '<', '<=', '>', '>=', 'LIKE', 'RLIKE', 'IS NULL', 'IS NOT NULL' And the 3rd value will be quoted automatically if it's not a placeholder, and if 2nd argument is a LIKE, will be also converted with DB::like(). |
1
2
3
$result = DB::delete("forum_topic") ->where( [["id", "=", 3]]) ->execute();
public orderBy( string/array $fields );
Add ORDER BY fields to a sentance.
string/fields $fields | Comma separated list or array of fields to order by |
DB | SQL sentance object |
1
2
3
$result = DB::select("forum_topic", "f") ->orderBy( ["f.id", "f.title DESC"] ) ->execute();
public groupBy( string/array $fields );
Add GROUP BY fields to a sentance.
string/fields $fields | Comma separated list or array of fields to group by |
DB | SQL sentance object |
1
2
3
$result = DB::select("forum_topic", "f") ->groupBy( ["f.category"] ) ->execute();
public having( string/array $wheres, string $operator );
Add HAVING clause(s) to a sentance.
string/array $wheres | Where clase expression(s) to add |
string $operator | Operator, one of 'AND' or 'OR'. Defaults to 'AND' |
DB | SQL sentance object |
1
2
3
$result = DB::select("forum_topic") ->having( [["id", "=", 3]]) ->execute();
public limit( int $limit );
Add LIMIT to a sentance. Set the maximum number of rows returned.
int $limit | Maximum number of rows |
DB | SQL sentance object |
1
2
3
$result = DB::select("forum_topic", "f") ->limit( 100 ) ->execute();
public offset( int $offset );
Add OFFSET to a sentance. Skip the first given number rows returned.
int $offset | Number of rows to skip |
DB | SQL sentance object |
1
2
3
$result = DB::select("forum_topic", "f") ->limit( 100 )->offset( 10 ) ->execute();
public sql( );
Convert SQL sentance object into a string.
None.
string | SQL sentance string |
1
2
3
4
$sql = DB::delete("forum_topic") ->where( [["id", "=", 3]]) ->sql(); echo ( DB::delete("forum_topic") );
public execute( array $arguments, int $selector );
Execute the SQL sentance on a datasource. If you have a key value array, you'll need with()
array $arguments | Values for the placeholders. Field names passed with fields(). |
string $selector | Execute on a specific datasource rather than the current one |
array | If the sentance was a SELECT query, the result set |
int | Otherwise the number of affected rows |
1
2
3
$result = DB::select("forum_topic", "ft") ->table( "forum_posts", "fp") ->execute();
public with( array $values, int $selector );
Execute the SQL sentance on a datasource with given values. The with() method basicaly is the same as calling fields() and execute().
array $values | Field names in array keys |
string $selector | Execute on a specific datasource rather than the current one |
array | If the sentance was a SELECT query, the result set |
int | Otherwise the number of affected rows |
1
2
3
4
5
6
7
$result = DB::update("forum_topic", "f")->with( [ "f.id" => 3, "f.title" => "New title" ]); // note the fields() method $result = DB::update("forum_topic", "f") ->fields( ["f.id", "f.title"]) ->execute([ 3, "New title"]);
$email = new Email( ); $email = new Email( string $saved );
Creates a new Email object.
string $saved | Email dump previously saved by get(). |
Email object |
1
$email = new Email();
public get( );
Dumps and Email object to be stored in database. You can recreate the Email object by providing this dump to a new instance.
None.
string | JSON encoded reprezentasion of the Email object |
1
2
$email = new Email(); $dump = $email->get();
public to( string $address );
Set primary recipient of the email.
string $address | Email address |
Email object |
1
$email->to("SomeBody <somebody@somewhere.com>");
public replyTo( string $address );
Set reply address of the email.
string $address | Email address |
Email object |
1
$email->replyTo("SomeBody <somebody@somewhere.com>");
public cc( string $address );
Set carbon copy address of the email.
string $address | Email address |
Email object |
1
$email->cc("SomeBody <somebody@somewhere.com>");
public bcc( string $address );
Set the hidden carbon copy address of the email.
string $address | Email address |
Email object |
1
$email->bcc("SomeBody <somebody@somewhere.com>");
public subject( string $subject );
Set subject for the email.
string $subject | Subject. Never leave it empty. |
Email object |
1
$email->subject("Something");
public message( string $message );
Set message body for the email.
string $message | Raw email body. |
Email object |
1
$email->message("Dear Santa,\n\nI was a good boy!");
public template( string $template, array $arguments );
Set message body for the email using a view template.
string $template | Template to use to generate email body. |
array $arguments | Arguments for the template. Refer them with email. prefix. |
Email object |
1
$email->template("email_order");
public attachFile( string $file, string $mimetype );
Attach a local file to the email.
string $file | Path and filename. |
string $mimetype | Mime type of the file. If not given, it will be autodetected. |
Email object |
1
$email->attachFile("data/invoices/1234.pdf");
public attachData( string $data, string $mimetype, string $filename );
Attach a file to the email from memory.
string $data | File contents in memory. |
string $mimetype | Mime type of the file. Defaults to 'application/octet-stream'. |
string $filename | Filename to use for the attachment. |
Email object |
1
2
3
4
ob_start(); imagepng($img); $image = ob_get_clean(); $email->attachData($image, "image/png", "welcome.png");
public send( string $backend );
Send out the email.
string $backend | Email backend. If not given, the one given in configuration will be used. |
boolean | True on success |
1
$email->send("phpmailer");
public static RPiPCB( );
Return Raspberry Pi Board Revision number from /proc/cpuinfo
.
None.
int | Board Revision number |
1
$pcb = GPIO::RPiPCB();
public static temp( );
Return Raspberry Pi's temperature from /sys/class/thermal/thermal_zone0
.
None.
float | Board temperature in SI oC. |
1
$SITemp = GPIO::temp();
public static freq( );
Return Raspberry Pi's CPU frequency from /sys/devices/system/cpu/cpu0/cpufreq
.
None.
float | CPU frequency. |
1
$freq = GPIO::freq();
public static reset( );
Reset all GPIO ports to input mode and unexport them from userspace.
None.
None.
1
GPIO::reset();
public static mode( int $pin, string $direction);
Reset all GPIO ports to input mode and unexport them from userspace.
int $pin | Pin number |
string $direction | The string 'in' or 'out'. Defaults to 'out'. |
GPIO | GPIO instance |
1
GPIO::mode(1, "out")->mode(2, "out");
public static set( int $pin, boolean $value);
Write to a GPIO port.
int $pin | Pin number |
boolean $value | Set port to high on true. Defaults to true. |
GPIO | GPIO instance |
1
GPIO::mode(1, "out")->set(1, true);
public static get( int $pin);
Read a GPIO port's current level.
int $pin | Pin number |
boolean | True if the port level is high, false if it's low. |
1
$pressed = GPIO::get(1);