yf_docs user module| File | plugins/docs/modules/yf_docs.class.php |
|---|---|
| Use | module('docs'), URL ?object=docs |
A site sets them in its config with $PROJECT_CONF['docs']['NAME'] = ...;
| Property | Default | Description |
|---|---|---|
$HARDENarray |
['csrf_admin' => ['title' => 'CSRF tokens in the admin', 'note' => ' (the user side has them already)', 'snippet' => '$PROJECT_CONF['csrf_guard']['ENABLED_FOR'] = ['admin' => true, 'user' => true];... |
The opt-in security switches of the Install page (Harden) and of the security/flags demo: title, note (trusted HTML), the project config snippet and the [class, property, class dir] it sets, for the default and the current value |
$HOME_SECTIONSarray |
|
Section cards of the home page, in this order (sections not listed follow by name): title, Font Awesome icon, one line |
$NOT_SECTIONSarray |
[] |
Public methods that are endpoints, not sections |
$SAMPLE_NAVBAR_BOXESarray |
['country', 'language', 'currency', 'timezone'] |
Selectors of the navbar sample _sample_navbar(), in this order: country, language, currency, timezone ($PROJECT_CONF['docs']['SAMPLE_NAVBAR_BOXES']) |
$SHOW_GITHUB_LINKSbool |
false |
Show the GitHub links and buttons on docs pages ($PROJECT_CONF['docs']['SHOW_GITHUB_LINKS']); off by default |
$demo_dir |
null |
|
$docs_dir |
null |
public function aliases()
public function aliases()
{
return $this->_subclass(__FUNCTION__);
}
public function all()
public function all()
{
return $this->_subclass(__FUNCTION__);
}
public function api()
API reference: /docs/api (index), /docs/api/functions, /docs/api/<class> (yf_docs_api).
public function api()
{
$id = $_GET['id'] ?? '';
return _class('docs_api')->show(is_string($id) ? $id : '');
}
public function assets()
public function assets()
{
return $this->_subclass(__FUNCTION__);
}
public function core_api()
public function core_api()
{
return $this->_subclass(__FUNCTION__);
}
public function demo()
public function demo()
{
return $this->_subclass(__FUNCTION__);
}
public function form()
public function form()
{
return $this->_subclass(__FUNCTION__);
}
public function html()
public function html()
{
return $this->_subclass(__FUNCTION__);
}
public function install()
Install section: requirements, getting yf, a project skeleton, web server configs, Docker and the hardening flags.
public function install()
{
return tpl()->parse('docs/install', ['harden' => $this->_harden_rows(), 'flags_link' => url('/@object/demo/' . urlencode('security/flags'))]);
}
public function services()
public function services()
{
return $this->_subclass(__FUNCTION__);
}
public function show()
public function show()
{
$sections = $this->_sections();
$id = (string) ($_GET['id'] ?? '');
// an unknown or removed section shows the home page
if ($id !== '' && isset($sections[$id])) {
return $this->$id();
}
// the old sections that became API pages (db, main, validate, ...) show the API page of their class
$api = _class('docs_api');
if ($id !== '' && isset($api->SECTION_ALIASES[$id])) {
return $api->show($api->SECTION_ALIASES[$id]);
}
return $this->_home();
}
public function table()
public function table()
{
return $this->_subclass(__FUNCTION__);
}
_ hooks and helpers; not routable as page actionspublic function __call($name, $args)
Catch missing method call.
public function __call($name, $args)
{
return main()->extend_call($this, $name, $args);
}
public function _bs_switch_url($bs, $uri = null)
The current request URI with bs=$bs in place of any bs parameter; the other parameters stay as they are.
$bs string '3' or '5', anything else is '3'$uri null|string request URI, default $_SERVER['REQUEST_URI'] public function _bs_switch_url($bs, $uri = null)
{
$uri = explode('#', (string) ($uri ?? $_SERVER['REQUEST_URI'] ?? '/'), 2)[0];
[$path, $query] = explode('?', $uri, 2) + [1 => ''];
$pairs = [];
foreach (explode('&', $query) as $pair) {
// bs, bs[] and bs[x] are all $_GET['bs'] for PHP
if ($pair !== '' && explode('[', urldecode(explode('=', $pair, 2)[0]), 2)[0] !== 'bs') {
$pairs[] = $pair;
}
}
$pairs[] = 'bs=' . ((string) $bs === '5' ? '5' : '3');
// a path, never a host: "//evil.example/" or "/\evil.example/" would be protocol-relative links
return '/' . ltrim($path, '/\\') . '?' . implode('&', $pairs);
}
public function _bs_switcher()
Navbar dropdown that switches the docs / demo site between Bootstrap 3 and 5. The front controller reads ?bs= and the yf_bs cookie (.dev/sample_app/router.php); a site shows the dropdown with {execute(docs,_bs_switcher)} in its navbar_additions template.
public function _bs_switcher()
{
return tpl()->parse('docs/bs_switcher', [
'cur' => conf('css_framework') === 'bs5' ? '5' : '3',
'url_3' => htmlspecialchars($this->_bs_switch_url('3'), ENT_QUOTES),
'url_5' => htmlspecialchars($this->_bs_switch_url('5'), ENT_QUOTES),
]);
}
public function _flag_values($class, $property, $dir = 'classes/')
Default (the class property) and configured value ($PROJECT_CONF) of one switch of $HARDEN; the class file is loaded, not instantiated.
$class string class name without the yf_ prefix$property string$dir string class dir inside the framework or the plugin: classes/, classes/auth/, ... public function _flag_values($class, $property, $dir = 'classes/')
{
$loaded = main()->load_class_file($class, $dir);
$vars = $loaded ? get_class_vars($loaded) : [];
$default = $vars[$property] ?? null;
$conf = $GLOBALS['PROJECT_CONF'][$class] ?? [];
return [
'default' => $default,
'current' => is_array($conf) && array_key_exists($property, $conf) ? $conf[$property] : $default,
'known' => array_key_exists($property, $vars),
];
}
public function _github_btn($info)
GitHub source button for a file / line, or '' when SHOW_GITHUB_LINKS is off.
$info mixed ['file' => ..., 'line_start' => ...] public function _github_btn($info)
{
return $this->SHOW_GITHUB_LINKS ? (string) _class('core_api')->_github_link_btn($info) : '';
}
public function _github_link($path = '')
public function _github_link($path = '')
{
if ( ! $this->SHOW_GITHUB_LINKS || ! strlen($path)) {
return '';
}
$path = realpath($path);
$yf_path = realpath(YF_PATH);
$yf_len = strlen($yf_path);
if (substr($path, 0, $yf_len) === $yf_path) {
$path = substr($path, $yf_len);
}
$link = _class('core_api')->GITHUB_BASE . trim($path, '/');
return '<i class="fa fa-github fa-lg"></i> <a href="' . $link . '">' . $link . '</a>';
}
public function _harden_rows()
Rows of the Harden table: the snippet escaped (braces too, the value goes through a template), the note is trusted HTML.
public function _harden_rows()
{
$rows = [];
foreach ((array) $this->HARDEN as $name => $info) {
$rows[$name] = [
'title' => _prepare_html((string) $info['title'], 0),
'note' => (string) ($info['note'] ?? ''),
'snippet' => _prepare_html((string) $info['snippet'], 0),
];
}
return $rows;
}
public function _home()
Home page: what yf is, its features, the supported versions and a card per section (template docs/home).
public function _home()
{
$sections = $this->_sections();
$cards = [];
foreach (array_keys((array) $this->HOME_SECTIONS + $sections) as $name) {
if ( ! isset($sections[$name])) {
continue;
}
$info = (array) ($this->HOME_SECTIONS[$name] ?? []);
$cards[$name] = [
'title' => _prepare_html((string) ($info['title'] ?? ucfirst(str_replace('_', ' ', $name)))),
'icon' => preg_replace('~[^a-z0-9-]~', '', (string) ($info['icon'] ?? 'fa-book')),
'desc' => _prepare_html((string) ($info['desc'] ?? '')),
'link' => url('/@object/' . $name),
];
}
return tpl()->parse('docs/home', [
'sections' => $cards,
'install_link' => url('/@object/install'),
'demo_link' => url('/@object/demo'),
'form_link' => url('/@object/form'),
'table_link' => url('/@object/table'),
'github_link' => preg_replace('~/(blob|tree)/.*$~', '', (string) _class('core_api')->GITHUB_BASE),
'show_github' => $this->SHOW_GITHUB_LINKS ? 1 : 0,
]);
}
public function _hook_side_column()
public function _hook_side_column()
{
$sections = $this->_sections();
$action = (string) ($_GET['action'] ?? '');
$hook_name = __FUNCTION__;
$api = _class('docs_api');
if ($action === 'api' || isset($api->SECTION_ALIASES[$action])) {
return $api->$hook_name();
}
// Try to load side column hook from subclass
if (isset($sections[$action])) {
// class should be instantinated with full path before this
$custom_obj = _class_safe('sample_' . $action);
if (is_object($custom_obj) && method_exists($custom_obj, $hook_name)) {
$custom = $custom_obj->$hook_name();
if ($custom) {
return $custom;
}
}
}
$links = [];
foreach ($sections as $name) {
$links[url('/@object/' . $name)] = t($name);
}
return html()->navlist($links);
}
public function _init()
public function _init()
{
_class('core_api')->add_syntax_highlighter();
$this->docs_dir = YF_PATH . '.dev/docs/';
$this->demo_dir = YF_PATH . '.dev/demo/';
tpl()->add_function_callback('github', function ($m, $r, $name, $_this) {
return $this->SHOW_GITHUB_LINKS ? _class('core_api')->get_github_link($m[2]) : '';
});
tpl()->add_function_callback('self_source', function ($m, $r, $name, $_this) {
});
css($this->_layout_css());
}
public function _layout_css()
Docs pages only (body class get-object-docs): long sample names wrap inside the side column instead of overlapping (Bootstrap 3 nav-list rows are 20px high) or pushing the chevron onto its own line (Bootstrap 5), and the crowded Bootstrap 5 navbar wraps its menu instead of the breadcrumb.
public function _layout_css()
{
return '.get-object-docs .left_area .bs-docs-sidenav > li { height:auto; min-height:20px; }
.get-object-docs .left_area .bs-docs-sidenav > li > a, .get-object-docs .left_area .nav.flex-column .nav-link { display:flex; align-items:baseline; gap:.35em; overflow-wrap:anywhere; }
.get-object-docs .left_area .bs-docs-sidenav .icon-chevron-right { float:none; flex:0 0 auto; margin-right:0; }
.get-object-docs .left_area .nav.flex-column .nav-link { padding:.15rem 0; }
.get-object-docs .left_area .nav.flex-column .nav-link > .fa { flex:0 0 auto; font-size:.7em; opacity:.5; }
.cssfw-bs5.get-object-docs .navbar .breadcrumb { flex-wrap:nowrap; white-space:nowrap; }
.cssfw-bs5.get-object-docs .navbar-collapse, .cssfw-bs5.get-object-docs .navbar-nav { flex-wrap:wrap; }';
}
public function _module_action_handler($name)
Catch all methods calls.
public function _module_action_handler($name)
{
if ($name[0] !== '_' && method_exists($this, $name)) {
return $this->$name();
}
$_GET['id'] = $name;
// view() may still be supplied by a site override or main()->extend()
if (method_exists($this, 'view') || isset(main()->_extend[main()->get_class_name($this)]['view'])) {
return $this->view();
}
return $this->show();
}
public function _sample_side_column($sample_obj, $names = null, $link_prefix = '/@object/@action/', $skip = [])
Side column of a sample section: one link per sample, nothing for the methods it has no sample of. Returns '' when there is nothing to list, so the section list is shown instead.
$sample_obj object sample class, its public methods are the samples$names array|null [link id => label] to list instead of the methods$link_prefix string url() path the link id is appended to$skip array methods that are not samples public function _sample_side_column($sample_obj, $names = null, $link_prefix = '/@object/@action/', $skip = [])
{
if ($names === null) {
$names = [];
foreach (get_class_methods($sample_obj) as $name) {
if ($name == 'show' || substr($name, 0, 1) == '_' || in_array($name, (array) $skip, true)) {
continue;
}
$names[$name] = $name;
}
}
ksort($names);
$items = [];
foreach ((array) $names as $id => $label) {
$items[] = [
// a zero-width space lets a long path such as form/boxes/datetime_select wrap after its slashes
'name' => str_replace('/', "/\u{200B}", (string) $label),
'link' => url($link_prefix . urlencode($id)),
];
}
return $items ? _class('html')->navlist($items) : '';
}
public function _sections()
Section names: the public methods of this module, minus the endpoints.
public function _sections()
{
$sections = [];
foreach (get_class_methods($this) as $m) {
if ($m[0] === '_' || $m === 'show' || in_array($m, (array) $this->NOT_SECTIONS, true)) {
continue;
}
$sections[$m] = $m;
}
ksort($sections);
return $sections;
}
public function _show_for($obj, $id = '', $target = '')
Render one sample method of a section, with its source and the source and docs of the same method of $target.
$obj object sample class$id string sample method, the first one when empty$target string class the section documents (html, assets, ...); empty for plain functions (aliases) public function _show_for($obj, $id = '', $target = '')
{
$id = (string) ($id ?: ($_GET['id'] ?? ''));
$methods = [];
foreach (get_class_methods($obj) as $name) {
if ($name == 'show' || substr($name, 0, 1) == '_') {
continue;
}
$methods[$name] = $name;
}
ksort($methods);
$name = $id !== '' ? $id : (string) current($methods);
if ( ! isset($methods[$name])) {
return _404();
}
$api = _class('core_api');
$self_source = $this->_source_text($api->get_method_source($obj, $name));
$target_info = '';
$target_docs = '';
if ($target !== '') {
// an object, so that a class outside modules/ (html lives in classes/) resolves too
$target_info = $api->get_method_source(_class($target), $name);
$target_docs = (string) $api->get_method_docs($target, $name);
} elseif (function_exists($name)) {
$target_info = $api->get_function_source($name);
}
$target_source = $this->_source_text($target_info);
$target_label = $target !== '' ? '_class("' . $target . '")->' . $name . '()' : 'function ' . $name . '()';
$github = is_array($target_info) ? $this->_github_btn($target_info) : '';
return
'<div id="head_' . $name . '" class="panel">
<div class="panel-heading">
<h1 class="panel-title">
<a href="' . url('/@object/@action/' . $name) . '">' . $name . '</a>
<div class="pull-right">
<button class="btn btn-primary btn-xs" data-toggle="collapse" data-target="#func_self_source_' . $name . '"><i class="fa fa-file-text-o"></i> ' . $name . '()</button> '
. ($target_source !== '' ? ' <button class="btn btn-primary btn-xs" data-toggle="collapse" data-target="#func_target_source_' . $name . '"><i class="fa fa-file-text-o"></i> ' . $target_label . '</button> ' : '')
. $github
. ($target_docs !== '' ? ' <button class="btn btn-primary btn-xs" data-toggle="collapse" data-target="#func_target_docs_' . $name . '">' . $target . '::' . $name . ' docs</button> ' : '')
. '</div>
</h1>
</div>
<div id="func_self_source_' . $name . '" class="panel-body collapse out"><pre class="prettyprint lang-php"><code>' . _prepare_html($self_source, 0) . '</code></pre></div> '
. ($target_source !== '' ? '<div id="func_target_source_' . $name . '" class="panel-body collapse out"><pre class="prettyprint lang-php"><code>' . _prepare_html($target_source, 0) . '</code></pre></div> ' : '')
. ($target_docs !== '' ? '<div id="func_target_docs_' . $name . '" class="panel-body collapse out">' . _class('html')->well(nl2br($target_docs)) . '</div> ' : '')
. '</div>
<section class="page-contents" id="func_out_' . $name . '">' . $obj->$name() . '</section>
';
}
public function _source_text($info)
core_api::get_method_source() returns '' for an unknown method.
public function _source_text($info)
{
return is_array($info) ? (string) ($info['source'] ?? '') : '';
}
public function _subclass($name)
public function _subclass($name)
{
return _class('sample_' . $name, YF_PATH . '.dev/samples/classes/')->show();
}