Phad: Php Html Api Database

Full CRUD with pure HTML. Php Code integrates seamlessly.

A framework for writing database driven web-apps in pure html


  • Commit 912d622 added breaking changes regarding can_read_row(). Commit c3f423b is the commit before that. If you updated on branch v0.4 and things broke, you can revert to c3f423b. Alternatively, make sure your can_read_row() handler accepts a string as the 3rd argument, AND make sure can_read_row return true for any node where can_read_row was not set on the node. THEN, commit c6dd271 removes can_read_row() from the compiled output & instead checks can_read_row during read_data(). This adds overhead of looping over every row before data is returned.

Notice: Liaison dependency

This currently depends upon [php/liaison]( for routing & for a couple other things. I intend to remove this dependency in the future. The routing will likely ALWAYS require an external router integration ... I'll just hopefully make that easier in the future.

Status: Under Development, nearing production usability

This is still under development, but many of the features are functional. There are many performance improvements to be found through caching. There are still featuers to add in. It is highly tested, though not completely.


Everything is HTML centric. Everything is extensible via PHP. You can write SQL queries, too.

  • Write pure HTML to load forms in modals (or their own pages)
  • Form validation, just based upon html nodes & attributes
  • Access controls in html, via custom nodes

Documentation | Getting Started

There are so many more featuers than what is listed here.

Sample View

You can see many more example views in the tests at test/Server/phad and test/input/views

<route pattern="/blog-list/"></route>  
<div item="Blog" >  
    <h1 prop="title"></h1>  
    <p prop="body"></p>  

Sample Form

You can see many more example forms in the tests at test/Server/phad/form and test/input/views/Form.

<route pattern="/blog/make/"></route>  
<form cansubmit="call:can_submit" item="Blog" target="/blog/{slug}/">  
        $slug = strtolower($BlogRow['title']);  
        $slug = str_replace(' ', '-', $slug);  
        $BlogRow['slug'] = $slug;  
        // unset($BlogInfo->args['phad']);  
        // print_r($BlogInfo);  
        // print_r($_POST);  
        // // exit;  
        // print_r($BlogRow);  
        // // exit;  
    <input type="text" name="title" maxlength="75">  
    <textarea name="body" maxlength="2000" minlength="10"></textarea>  
    <input type="backend" name="slug" minlength=4 maxlength=150 />  

You'll need to add a submit button ...

Server Setup

Example setup code with Liaison

$_GET['user'] = $_GET['user'] ?? 'default-user-role';  
$options = [  
    'pdo' => $lildb->pdo(), // a pdo object  
    // 'user' => require(__DIR__.'/phad-user.php'), // a user object (no interface available ...)  
    'router' => $router,  
$phad = \Phad::main($options);  
$phad->filters['markdown'] = function($v){return 'pretend-this-is-markdown:<p>'.$v.'</p>';};  
$phad->integration->setup_liaison_route($lia, '/sitemap.xml', $phad->sitemap_dir.'/sitemap.xml');  
// $custom_access = require(__DIR__.'/phad-access.php'); // returns an Access object  
// $phad->access = $custom_access;  
$phad->access_handlers['main_msg'] =   
        echo "This is my custom deletion response. I don't care if deletion succeeded. Id was ".$ItemInfo->args['id'];  
        return false;  
$phad->access_handlers['never_allow'] =   
        return false;  
$phad->access_handlers['can_submit'] =   
    function($ItemInfo, $ItemRow){  
        if (isset($_GET['deny_access'])&&$_GET['deny_access']=='true')return false;  
        return true;  
$phad->access_handlers['permit_me'] =   
    function($data_node, $ItemInfo){  
        if ($_GET['permit_me']=='true')return true;  
        return false;  
$phad->handlers['user_has_role'] =   
function(string $role){  
    if (isset($_GET['user'])&&$role == $_GET['user'])return true;  
    return false;  
$phad->handlers['can_read_row'] =   
    function(array $ItemRow,object $ItemInfo,string $ItemName){  
        if (!isset($_GET['title']))return true;  
        if ($ItemRow['title'] == $_GET['title']){  
            return true;  
        return false;  

Upload Files

This isn't integrated well, yet. You have to add some code to your form, like:

<route pattern="/document/make/"></route>  
<form item="Document" target="/document-list/">  
        $DocumentRow['file_name'] = $_FILES['doc']['name'];  
        $DocumentRow['stored_name'] = \Phad\PDOSubmitter::uploadFile($_FILES['doc'],   
            dirname(__DIR__, 2).'/files-uploaded/',  
    <input type="text" name="title" maxlength="75" />  
    <input type="file" name="doc" />  
    <input type="backend" name="file_name" />  
    <input type="backend" name="stored_name" />  

Extended Documentation

Executing it

  • $item = $phad->item('item/name', ['key'=>'value']);: load a phad item
  • $item->html(): return a string of the finished item
  • EXPERIMENTAL, pass ':data'=>'NAME' in arguments to specify the name of a p-data node to use. Declare attribute name="NAME" on the <p-data> node to match. This usage may change in future versions.
  • DEPRECATED, pass ''=>'NAME' to do the same a :data

Just, other stuff

  • $phad->exit_on_redirect = false to stop redirects from exiting

Simple Example

<route pattern="/blog/{slug}/"></route>  
<div item="Blog" >  
    <p-data where="Blog.slug LIKE :slug"></p-data>  
    <h1 prop="title"></h1>  
    <x-prop prop="body" filter="commonmark:markdownToHtml"></x-prop>  

Basics / Item Nodes / Some forms tuff

  • Ex: <div item="Blog">
  • Ex: <h1 prop="title"> inside the blog item div
  • Variables available: object $Blog, array $BlogRow, stdClass $BlogInfo ... there's more ... see your compiled output
  • <h1 prop="title" filter="html_escape"> to apply html_escape filter before dispalying title. (see filter documentation)
  • add loop="inner" to loop INSIDE the div (so the div only displays once, but it's content shows for each row)
  • pass ['Blog'=>$Blog] or ['BlogList'=>[$Blog1, $Blog2]] to use that as data (skips access checks, since it uses the default data node)
  • <x-item item="Blog"> ... works like any other item node, except x-item will hide itself
  • <x-prop prop="body"></x-prop> to display $Blog->body without showing an html node
  • override certain methods on Phad to further customize things ....
  • delete Filter controller. I'm not using it ... but it might be in some tests? ... phad just directly handles filters now

Phad Overrides

Of course, you can override any part of Phad ...

  • function object_from_row(array $row, $ItemInfo): object (default returns (object)$ItemRow)
    • Custom object can add properties with <p prop="some_prop"> without the prop being in the db
  • function onSubmit($ItemInfo, &$ItemRow): bool, false to stop submission
  • function onWillDelete($ItemInfo): bool, false to stop deletion

Phad Handlers

Set $phad->handlers['handler_name'] = function(...$args){} ... then $phad->handler_name() will call that function.
The function name can be anything, just need any callable.

  • 'can_read_row' (optional): function can_read_row(array $ItemRow,object $ItemInfo,string $ItemName): bool ... returns true by default
  • 'item_initialized' (required?): function item_initialized(stdClass $ItemInfo): void
  • 'user_has_role' (required?): function user_has_role(string $roles): bool where $roles should be like guest|admin|moderator (though that's up to you & how you define your role access in attribute handlers)


  • <route pattern="/some/route/">
  • <route pattern="/some/{slug}/>" for dynamic routes. Requires a data node like <p-data where="Blog.slug LIKE :slug"></p-data>


  • Goes inside a <route> node. Can use dynamic patterns
  • For <route pattern="/some/{slug}/": <sitemap sql="SELECT slug FROM blog"> ...
  • The <sitemap node can declare attributes and/or the sql can select priority, lastmod, and changefreq
  • @todo <sitemap handler="handler_name" points to $phad->sitemap->handlers['handler_name'] and ... idk ... feature not implemented yet
  • @todo allow individual route sitemapping like For <route pattern="/some/route/": <sitemap></sitemap> ... hack this by setting sql="SELECT 1 as one on the sitemap node ...

Property filters

  • Ex: <p prop="description" filter="my_filter"> yields <p><?=$phad->filter($Blog->description)?></p>
  • commonmark:markdownToHtml uses "league/commonmark": "^1.0" ... for now ... which you have to add to your composer.json bc the dependency is in require-dev for this package
  • add other filters with $phad->filters['filter_name'] = function($property_value){}

<on> nodes

  • if one <p-data> node is granted, then only the successful's 200 status will be displayed
  • if no <p-data> nodes are granted, then each data node's <on> node will display ... showing 403, 404, 500, etc ... depending what the error was for that node
  • what about <on> nodes not nested in <p-data>? I'm not sure.

<p-data> nodes

  • must be direct child of an item node
  • sql attribute to craft a full query. (you can use multiple lines inside the double quotes)
  • where, limit, orderby, and cols attributes to refine if not using sql attribute. do not include the sql verb inside the double quotes
  • access attribute used to limit access. See the docs on attribute call handlers
  • if attribute may contain php code such as isset($some_var). This code will be eval'd & if it returns false, then this data node will not be used.
  • data_loader="some_key" can be used to load data by defining $phad->data_loaders["some_key"] = function(DomNode, ItemInfo).

Hook Nodes

all hook nodes can contain php code, html, whatever. For a better understanding of these, use them & look at the compiled output. the submit nodes are all for forms only.

  • <onsubmit>php code: Set $ItemInfo->mode = null to stop submission or modify $ItemRow to change what gets submitted
  • <didsubmit>php code
  • <failsubmit> php code
  • <diddelete> php code: for code to run only AFTER the database row is deleted
  • <willdelete> php code: For code to run BEFORE the database row is deleted
  • <on s=404|403|500|200> php code ... as direct child of <p-data> or direct child of <div item="Blog">

Attribute Call Handlers

cansubmit, candelete, and diddelete all go on <form item="Blog"> nodes.

These take strings like role:moderator;call:handler_name. Access handlers should return true/false. Hook handlers (diddelete) do not need to return anything.

For handler_name, do $phad->access_handlers['handler_name'] = function(...$args){}

  • cansubmit: function(stdClass $ItemInfo, array $RowToStore): bool
  • candelete: function(stdClass $ItemInfo): bool
  • diddelete: function(stdClass $ItemInfo): void
  • <button access="call:can_do_buttons">: function(array $node_info): bool (for can_read_node()) ... $node_info is the html node's attributes + tagName
  • <p-data access="call:is_data_allowed">: function(array $data_node_info, stdClass $ItemInfo): bool where $data_node_info is the html node's attributes + tagName


  • add target="/blogs/{slug}/" to form node to automatically redirect after submission. the slug will be filled in by the submitted row
  • See hook nodes & attribute call handlers
  • To delete a field request /page/?phad_action=delete&id=ID_TO_DELETE
  • To enable deletion, add candelete attribute to form. If empty, there will be no checks & deletion will always succeed. candelete="false" declines deletion. candelete='role:admin' or candelete="call:your_func" for checks (see Attribute Call handlers)
  • <errors></errors> node as direct child of the form will automatically display errors in a div with class="errors" and each message is in a <p> with no class.
  • in your attribute call handlers, do $Info->submit_errors[] = ['msg'=>"Some Message"]; to display in the <errors> node
  • manually displaying errors ... <?php foreach($ItemInfo->submit_errors as $m){echo $m['msg'];}
    • Or you can use the items feature: <div item="ItemSubmitErrors"><p prop="msg"></p></div> ... this method may be removed. idk. it isn't tested
  • file uploads: Kinda meh, see below
  • backend inputs: <input type="backend" name="slug"> ... You might use <onsubmit> to convert a title into a slug. To store the slug in the db, add $BlogRow['slug'] = $the_slug & the backend input so it passes validation. backend inputs are removed from the html.
  • @TODO <input type="hidden" name="id"> is added automatically
  • For inputs added via php (thus not in the html when compiled by phad), add to the onsubmit $ItemInfo->properties['prop'] => ['type'=>'text','tagName'=>'input']. You may change the type or add other attributes that exist for form inputs (such as minlength/maxlength, or required). the tagName=>'input' part is necessary for validation.
  • For submitted fields that you DON'T want in the database, do unset($ItemRow['field']) & in some cases unset($ItemInfo->properties['field']) in your <onsubmit> code

File Uploads

idk what to say. here's an example. Notice how i modify the document row & add the type=backend nodes

<route pattern="/document/make/"></route>  
<form item="Document" target="/document-list/">  
        $DocumentRow['file_name'] = $_FILES['doc']['name'];  
        $DocumentRow['stored_name'] = \Phad\PDOSubmitter::uploadFile($_FILES['doc'],   
            dirname(__DIR__, 2).'/files-uploaded/',  
    <input type="text" name="title" maxlength="75" />  
    <input type="file" name="doc" />  
    <input type="backend" name="file_name" />  
    <input type="backend" name="stored_name" />