Loading Data into Phad views & forms
Data can be passed directly into your views or loaded from the database when your views are loaded. You can modify how the data is loaded (such as convert a DATETIME string into a Date object), or convert returned rows into custom objects.
For changing what value is output into your view/form, you should use Property Filters.
Related: Property Filters, Routing and Sitemaps, Access Controls, Forms
Documentation
- Overview
- Data Nodes
- Using the data
- Handling and displaying errors
- Load item with custom data
- Access Controls on Data Nodes
- Queries in your templates
- Custom data loaders
- Print template with no data
Overview
-
<p-data>nodes support:name, queries w/ named:paramaters,data_loader,accesscontrols,if="eval'd PHP Code"for alternate access control, and<on>nodes for displaying errors. - An item named
Blogqueries tableblog, generates an array$BlogRow, and an object$Blog - Output data via:
-
<node prop="prop_name" filter="filter_name"></node>(filter is optional) -
<?=$Blog->prop_name?> -
<?=$BlogRow['prop_name']?>
-
-
<on>nodes supports=200,s=403, ands=404 - Manually load items with
$phad->item('blog/Post', $data)where data can be:-
['Blog'=> [...row...]], -
['BlogList'=>[row1, row2, row3,...]](a row CAN be['_object'=>object]) -
['_object'=> new YourBlogObject(...)]) -
[':data'=>'NAME', 'query_param'=>$value]
-
-
access="???"can be:-
role:USER_ROLE -
fn:\Some\Php\function_namethat returns booleantrue/false -
call:custom_handler
-
- Queries can be
sql="FULL QUERY"or use any/all verbs: cols, where, limit, orderby, join.- Add
paginate="INT"to paginate results for verb-based queries (does not work forsql="..."). Page is indicated$_GET['page']
- Add
-
data_loader="name"references$phad->data_loaders['name'] = function($DataNode, $ItemInfo, $phad): array -
<p-data type="black_hole">allows rendering of templates with no data. -
$args['arg_name']references params passed-in via$phad->item('ItemName', ['arg_name'=>$value])
Data Nodes
Data nodes (<p-data>) are how you define queries and custom data loaders to retrieve data. Each data node has a type, which typically does not need to be declared. The valid types are defined in Phad\enum\DataTypes. Data for those nodes (like :slug) can be passed in explicitly or auto-filled from the route.
Example:
<route pattern="/blog/{slug}/"></route>
<div item="Blog">
<p-data where="Blog.slug LIKE :slug"></p-data>
<p-data data_loader="get_archived_blog_post"></p-data>
... the view ...
</div>
A template can have multiple data nodes. When this is the case, they are processed in the order they are declared. When one returns data, that data is used and no other data nodes are processed, and any previous errors are ignored. If data is not returned, there is a 404 error. If access is denied, there is a 403 error.
Nodes are skipped when a named node is selected by the calling code, or if="..." returns false. (see below)
Note: To add your own data node types, you must override Phad::read_data().
Tip: Add loop="inner" to the item node to only print the outer HTML once.
Default Data Nodes
Every Item will have a default data node automatically added to it if one is not explicitly listed. For regular items, the default data node does not return any data. For Forms, the default data node returns a matching row if $_GET['id'] is set, or a \Phad\BlackHole object otherwise, which allows your form to be printed with no data. When a form is POSTed, the default data node returns the submitted data, which enables automatic submission (If submission fails, then the submitted data will be printed in the form).
Disable the default data node by adding it and setting access="false":
<form item="Blog">
<p-data type="default" access="false"></p-data>
<p-data where="Blog.slug LIKE :get.slug"></p-data>
... the view ...
</div>
Note: :get.slug will pull the value from $_GET['slug']
Named Data Nodes
Any data node can have a name attribute.
<div item="Blog">
<p-data name="published" where="Blog.status LIKE 'published'"></p-data>
<p-data name="draft" where="Blog.status LIKE 'draft'"></p-data>
<p-data name="any" where="Blog.status LIKE :status></p-data>
... the view ...
</div>
Then when loading the item manually, you can specify the name of the data node to use, and pass any additional data that node requires.
<?php
$item = $phad->item('blog/post_list', [':data' => 'any', 'status'=>'archived']);
echo $item->html();
This way, only the data node with name="any" will be executed. The other data nodes will be skipped.
Using the data
Data Nodes cause one ore more array rows of data to be loaded. This data is then converted into an object by Phad::object_from_row() (which you can override). You can then print the data by declaring the prop attribute on any html node, by referencing the object directly, or you can access the initial array row.
Rules:
- The object will have the same name as the item. Ex:
<article item="Blog">yields an object$Blog - The row is the item name +
"Row". Ex:<article item="Blog">yields array$BlogRow - A
propattribute loads the same-named property from the object:<h1 prop="title">yields<h1><?=$Blog->title?></h1> - Use
$args['arg_name']to reference params passed-in via$phad->item('ItemName', ['arg_name'=>$value])
Here is an example, which assumes you have overridden Phad::object_from_row() and are using a custom class for your blog item:
<route pattern="/blog/{slug}/"></route>
<article item="Blog">
<p-data where="slug LIKE :slug"></p-data>
<h1 prop="title" filter="all_caps"></h1>
<time datetime="<?=$Blog->datetime?>">
<?=$Blog->get_friendly_date()?>
</time>
<main style="color:<?=$args['color']?>;">
<?php
echo MyCustomClass::markdown_to_html(
$BlogRow['body']
);
?>
</main>
</article>
Note 1: See Property Filter for more info on filter="...".
Note 2: An info object also exists, with the item name + "Info", like $BlogInfo. This contains all the metadata for your item, and you can view it in the compiled output of your item.
Note 3: Instead of overriding Phad::object_from_row() you can do the conversion inside your view. Ex: $MyBlog = new MyBlogClass($BlogRow)
Handling and displaying errors
Declare <on s=HTTP_CODE> nodes under your items or data nodes to handle 404 data not found, 403 access denied, or 200 success outcomes. The <on> nodes are processed AFTER data is loaded, BEFORE data is displayed.
The <on> nodes can contain HTML and PHP. Any output will be ABOVE your item's HTML.
Example:
<div item="Blog">
<p-data name="first" where="Blog.type LIKE :type" access="role:admin" limit="0,2">
<on s=403><p>Not allowed to load blogs by type</p></on>
<on s=404><p>No blog posts found with type <?=$args['type']?></p></on>
</p-data>
<p-data name="second" where="Blog.title LIKE CONCAT('%', :title, '%')" limit="0,3">
<on s=404><p>No blog posts found matching title <?=$args['title']?></p></on>
</p-data>
<on s=403><p>Not allowed!</p></on>
<on s=200><p>You got blogs!</p></on>
<h1 prop="title"></h1>
<p prop="description"></p>
</div>
A Child <on> node (child of p-data) will execute when its parent data node generates a matching status. Multiple data nodes are typically processed, so multiple child on nodes process during one request.
The global <on> node (child of item) will execute once if any data nodes return a matching status (403 access denied). If a child on node has the same status (s=*) as the global on node, then BOTH will display. A single global on node will never display more than once.
Load item with custom data
Data can be loaded via PHP and then passed into your item instead of using any of the declared data nodes. You can pass a single item as an array, a list of items as an array of arrays, or an object.
The below examples reference this item template:
blog/main.php
<article item="Blog">
<h1 prop="title"></h1>
<main prop="body"></main>
</article>
Single Item
Pass an arg with the same name as the item you're loading.
<?php
$item = $phad->item('blog/main',
['Blog'=>
['title'=>'Bears are cool', 'body'=>'<p>And cute!</p>']
]
);
The passed in Blog row will be used as the data for the item.
Item List
Use the item's name, but add "List" to it, like "BlogList"
<?php
$item = $phad->item('blog/main',
['BlogList'=>
[
['title'=>'Bears are cool', 'body'=>'<p>And cute!</p>']
['title'=>'Bears are great', 'body'=>'<p>I want to scratch their bellies!</p>']
['title'=>'Bears are love', 'body'=>'<p>I want to cuddle them but i can\'t!</p>']
]
]
);
Object
Each of the rows in the examples above will be cast into objects automatically by Phad::object_from_row() (which you can override for custom object classes). You can alternatively pass objects in directly.
<?php
$item = $phad->item('blog/main',
['_object'=>
new MyCustomBlogObject('Bears are cool', '<p>And cute!</p>')
]
);
Mix and Match
In examples one and two, you can replace any of those rows with an object entry, and they'll work together seamlessly.
<?php
$item = $phad->item('blog/main',
['BlogList'=>
[
['title'=>'Bears are great', 'body'=>'<p>I want to scratch their bellies!</p>']
['_object' => new MyCustomBlogObject('Bears are cool', '<p>And cute!</p>')]
['title'=>'Bears are love', 'body'=>'<p>I want to cuddle them but i can\'t!</p>']
]
]
);
Access Controls on Data Nodes
See Access Controls for more information.
Access to data nodes can be controlled with any of the following methods. If access is denied by access="...", there will be a 403 error.
built-in role access handler
<div item="Blog">
<p-data where="..." access="role:USER_ROLE"></p-data>
...
</div>
call php function
Note: the called function must return strict boolean true/false.
<div item="Blog">
<p-data where="..." access="fn:\Your\Namespace\some_function_name"></p-data>
...
</div>
custom access handler
<div item="Blog">
<p-data where="..." access="call:HANDLER_NAME"></p-data>
...
</div>
if attribute
When if="..." returns false, the data node is SKIPPED, and there is no error.
<div item="Blog">
<p-data where="..." if="some_php_code_that_return_bool()"></p-data>
...
</div>
NOTICE: if is compiled into return (YOUR_CODE); and eval'd at runtime. Your code must return boolean true to allow access. All other values block access.
Queries in your templates
Data nodes can define queries for loading data. This can be written as full sql statements or specific attributes like where.
In these examples, bound paramaters (like :slug) can be auto-filled if {slug} is present in the item's route, or if slug is explicitly passed to the item when being loaded manually.
Paramaters can also be filled from GET params, by using :get.param_name to pull $_GET['param_name'] (SQL verbs example, under where).
Full SQL
With full SQL, you just write the full query within the sql attribute.
<route pattern="/blog/{slug}/"></route>
<div item="Blog">
<p-data sql="SELECT title, body, slug FROM blog WHERE slug LIKE :slug"></p-data>
...
</div>
SQL Verbs
For a shorthand, you can just include the specific SQL verbs you want added to the query. You can include just one, or as many as you need. The SELECT portion will be included automatically. The table name is the lowercase version of the item name, so item="Blog" becomes SELECT * FROM blog.
<route pattern="/blog-list/"></route>
<div item="Blog">
<p-data
cols="title, body, slug"
where="title LIKE CONCAT('%', :get.title, '%')"
limit="LIMIT 0, 10"
orderby="`date_created` DESC"
join="`table_name` tn ON tn.col = blog.col"
></p-data>
...
</div>
Pagination
You can paginate results with the paginate attribute, where the value is the int number of rows to return per page. $_GET['page'] will specify which page to query for, where 0 is the first page.
<route pattern="/blog-list/"></route>
<div item="Blog">
<p-data
where="title LIKE CONCAT('%', :get.title, '%')"
paginate="10"
></p-data>
...
<a href="/blog-list/?page=<?=intval($_GET['page']??0)+1?>">Next Page</a>
</div>
Now your result set is paginated. Adding next and previous links must be done manually, like above.
Custom data loaders
Data loaders are custom functions you write, then register on your Phad instance. Those data loaders can then be called by your data nodes to load data.
Sample item:
<div item="Blog">
<p-data data_loader="get_blog_post"></p-data>
... the view ...
</div>
Simple Singular Data Loader setup:
<?php
$pdo = ... your pdo instance ...
$phad->data_loaders['get_blog_post'] =
function(array $DataNode, object $ItemInfo, \Phad $phad): array {
$slug = $ItemInfo->args['slug'];
$stmt = $phad->pdo->prepare("SELECT * FROM `blog` WHERE `slug` LIKE :slug");
$stmt->execute(['slug'=>$slug]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
;
More automated setup of data loaders:
This approach allows you to simply define new methods in a single class without rewriting the Phad integration for each handler.
<?php
class MyDataLoaderClass {
... class properties ...
public function __construct(\Phad $phad, \PDO $pdo, ...){}
public function load_blogs(array $DataNode, object $ItemInfo): array {
// query & return rows
}
public function load_authors(array $DataNode, object $ItemInfo): array {
// ...
}
public function initialize_loaders(){
$methods = get_class_methods($this);
foreach ($methods as $m){
if (substr($m,0,5) != 'load_')continue;
$name = substr($m,5);
$this->phad->data_loaders['load:'.$name] = [$this, $m];
}
}
}
(new MyDataLoaderClass($phad, $pdo, ...))->initialize_loaders();
There are other approaches for doing this same thing, like using PHP 8's attributes, or having all handlers point to a single method, which then routes the data loader call.
Print template with no data
A \Phad\BlackHole object can be used to load a template without any data. Normally if there is no data, a template's contents won't be printed. The BlackHole object returns itself when any method is called, supports array access without error, returns null for any property access, and does nothing (no error or state change) when properties are set.
Manually pass the black hole object:
<?php
$item = $phad->item('blog/main',
['_object'=>
new \Phad\BlackHole()
]
);
Use a black_hole data node:
<div item="Blog">
<p-data type="black_hole"></p-data>
... the view ...
</div>