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
Warning
- Commit
912d622
added breaking changes regardingcan_read_row()
. Commitc3f423b
is the commit before that. If you updated on branch v0.4 and things broke, you can revert toc3f423b
. Alternatively, make sure yourcan_read_row()
handler accepts astring
as the 3rd argument, AND make surecan_read_row
returntrue
for any node wherecan_read_row
was not set on the node. THEN, commitc6dd271
removescan_read_row()
from the compiled output & instead checks can_read_row duringread_data()
. This adds overhead of looping over every row before data is returned.
Notice: Liaison dependency
This currently depends upon [php/liaison](https://tluf.me/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.
Features
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>
</div>
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}/">
<onsubmit><?php
$slug = strtolower($BlogRow['title']);
$slug = str_replace(' ', '-', $slug);
$BlogRow['slug'] = $slug;
?></onsubmit>
<failsubmit><?php
// unset($BlogInfo->args['phad']);
// print_r($BlogInfo);
// print_r($_POST);
// // exit;
// print_r($BlogRow);
// // exit;
?></failsubmit>
<input type="text" name="title" maxlength="75">
<textarea name="body" maxlength="2000" minlength="10"></textarea>
<input type="backend" name="slug" minlength=4 maxlength=150 />
</form>
You'll need to add a submit button ...
Server Setup
Example setup code with Liaison
<?php
$_GET['user'] = $_GET['user'] ?? 'default-user-role';
$options = [
'item_dir'=>$dir.'/phad/',
'cache_dir'=>$dir.'/cache/',
'sitemap_dir'=>$dir.'/sitemap/',
'pdo' => $lildb->pdo(), // a pdo object
// 'user' => require(__DIR__.'/phad-user.php'), // a user object (no interface available ...)
'router' => $router,
'throw_on_query_failure'=>true,
'force_compile'=>false,
];
$phad = \Phad::main($options);
$phad->filters['markdown'] = function($v){return 'pretend-this-is-markdown:<p>'.$v.'</p>';};
$phad->integration->setup_liaison_routes($lia);
$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'] =
function($ItemInfo){
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'] =
function($ItemInfo){
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/">
<onsubmit><?php
$DocumentRow['file_name'] = $_FILES['doc']['name'];
$DocumentRow['stored_name'] = \Phad\PDOSubmitter::uploadFile($_FILES['doc'],
dirname(__DIR__, 2).'/files-uploaded/',
['txt']
);
?></onsubmit>
<input type="text" name="title" maxlength="75" />
<input type="file" name="doc" />
<input type="backend" name="file_name" />
<input type="backend" name="stored_name" />
</form>
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 ap-data
node to use. Declare attributename="NAME"
on the<p-data>
node to match. This usage may change in future versions. - DEPRECATED, pass
'data.name'=>'NAME'
to do the same a:data
Just, other stuff
-
$phad->exit_on_redirect = false
to stop redirects fromexit
ing
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>
</div>
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 applyhtml_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, exceptx-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
- Custom object can add properties with
-
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 likeguest|admin|moderator
(though that's up to you & how you define your role access in attribute handlers)
Routes
-
<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>
Sitemaps
- 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 selectpriority
,lastmod
, andchangefreq
- @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 settingsql="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 inrequire-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's200
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
, andcols
attributes to refine if not usingsql
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 asisset($some_var)
. This code will beeval
'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
(forcan_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
Forms
- 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'
orcandelete="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 withclass="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
- Or you can use the items feature:
- 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
& thebackend
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 asminlength
/maxlength
, orrequired
). thetagName=>'input'
part is necessary for validation. - For submitted fields that you DON'T want in the database, do
unset($ItemRow['field'])
& in some casesunset($ItemInfo->properties['field'])
in your<onsubmit>
code
File Uploads
test/Server/phad/form/document.php
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/">
<onsubmit><?php
$DocumentRow['file_name'] = $_FILES['doc']['name'];
$DocumentRow['stored_name'] = \Phad\PDOSubmitter::uploadFile($_FILES['doc'],
dirname(__DIR__, 2).'/files-uploaded/',
['txt']
);
?></onsubmit>
<input type="text" name="title" maxlength="75" />
<input type="file" name="doc" />
<input type="backend" name="file_name" />
<input type="backend" name="stored_name" />
</form>