yf_docs user module

Fileplugins/docs/modules/yf_docs.class.php
Usemodule('docs'), URL ?object=docs

Public properties

A site sets them in its config with $PROJECT_CONF['docs']['NAME'] = ...;

PropertyDefaultDescription
$HARDEN
array
['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_SECTIONS
array
['install' => ['title' => 'Install', 'icon' => 'fa-download', 'desc' => 'Requirements, getting yf, a project skeleton, nginx / Cad'...], 'demo' => ['title' => 'Demos', 'icon' => 'fa-play', 'desc' => 'Interactive demos (validation, table paging, htmx, themes'...], 'form' => ['title' => 'Forms', 'icon' => 'fa-list-alt', 'desc' => 'form() builder demos: fields, boxes, validation, stacked '...], 'table' => ['title' => 'Tables', 'icon' => 'fa-table', 'desc' => 'table() builder demos: filters, sorting, paging and row a'...], 'html' => ['title' => 'HTML widgets', 'icon' => 'fa-th-large', 'desc' => 'html() widgets: tabs, accordion, modal, carousel, navbar,'...], 'assets' => ['title' => 'Assets', 'icon' => 'fa-file-code-o', 'desc' => 'The asset descriptors (CSS / JS packages) that asset() lo'...], ... (11 items)]
array (
  'install' => 
  array (
    'title' => 'Install',
    'icon' => 'fa-download',
    'desc' => 'Requirements, getting yf, a project skeleton, nginx / Caddy / Traefik / Envoy, Docker and the hardening flags.',
  ),
  'demo' => 
  array (
    'title' => 'Demos',
    'icon' => 'fa-play',
    'desc' => 'Interactive demos (validation, table paging, htmx, themes, SQL preview, CSRF, charts) and runnable examples, each with its source.',
  ),
  'form' => 
  array (
    'title' => 'Forms',
    'icon' => 'fa-list-alt',
    'desc' => 'form() builder demos: fields, boxes, validation, stacked rows, tabs.',
  ),
  'table' => 
  array (
    'title' => 'Tables',
    'icon' => 'fa-table',
    'desc' => 'table() builder demos: filters, sorting, paging and row actions.',
  ),
  'html' => 
  array (
    'title' => 'HTML widgets',
    'icon' => 'fa-th-large',
    'desc' => 'html() widgets: tabs, accordion, modal, carousel, navbar, lists and more.',
  ),
  'assets' => 
  array (
    'title' => 'Assets',
    'icon' => 'fa-file-code-o',
    'desc' => 'The asset descriptors (CSS / JS packages) that asset() loads, with demos.',
  ),
  'services' => 
  array (
    'title' => 'Services',
    'icon' => 'fa-cubes',
    'desc' => 'Third-party PHP libraries the framework loads on demand, and their locked versions.',
  ),
  'aliases' => 
  array (
    'title' => 'Aliases',
    'icon' => 'fa-link',
    'desc' => 'Global helper functions such as a() and tip().',
  ),
  'api' => 
  array (
    'title' => 'API reference',
    'icon' => 'fa-code',
    'desc' => 'Every framework class by plugin and the global helpers: signatures, docblocks, public properties and the source.',
  ),
  'core_api' => 
  array (
    'title' => 'Core API',
    'icon' => 'fa-sitemap',
    'desc' => 'Browse the framework classes, modules and functions with their source.',
  ),
  'all' => 
  array (
    'title' => 'Reference texts',
    'icon' => 'fa-book',
    'desc' => 'Longer texts: helpers, the html widgets, the query builder, UTF-8.',
  ),
)
Section cards of the home page, in this order (sections not listed follow by name): title, Font Awesome icon, one line
$NOT_SECTIONS
array
[] Public methods that are endpoints, not sections
$SAMPLE_NAVBAR_BOXES
array
['country', 'language', 'currency', 'timezone'] Selectors of the navbar sample _sample_navbar(), in this order: country, language, currency, timezone ($PROJECT_CONF['docs']['SAMPLE_NAVBAR_BOXES'])
$demo_dir null
$docs_dir null

Public methods 30

aliases

public function aliases()
    public function aliases()
    {
        return $this->_subclass(__FUNCTION__);
    }

all

public function all()
    public function all()
    {
        return $this->_subclass(__FUNCTION__);
    }

api

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 : '');
    }

assets

public function assets()
    public function assets()
    {
        return $this->_subclass(__FUNCTION__);
    }

core_api

public function core_api()
    public function core_api()
    {
        return $this->_subclass(__FUNCTION__);
    }

demo

public function demo()
    public function demo()
    {
        return $this->_subclass(__FUNCTION__);
    }

form

public function form()
    public function form()
    {
        return $this->_subclass(__FUNCTION__);
    }

html

public function html()
    public function html()
    {
        return $this->_subclass(__FUNCTION__);
    }

install

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'))]);
    }

services

public function services()
    public function services()
    {
        return $this->_subclass(__FUNCTION__);
    }

show

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();
    }

table

public function table()
    public function table()
    {
        return $this->_subclass(__FUNCTION__);
    }

Methods starting with _ hooks and helpers; not routable as page actions

__call

public 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);
    }

_bs_switcher

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),
        ]);
    }

_flag_values

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),
        ];
    }

_github_btn

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) : '';
    }

_harden_rows

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;
    }

_home

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);
    }

_init

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());
    }

_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_navbar()

Country / language / currency / timezone dropdowns for a navbar_additions template ({execute(docs,_sample_navbar)}), those of $SAMPLE_NAVBAR_BOXES only; the language one shows the current language.

    public function _sample_navbar()
    {
        $boxes = [
            'country' => ['country_box', 'US'],
            'language' => ['language_box', conf('language') ?: 'en'],
            'currency' => ['currency_box', 'UAH'],
            'timezone' => ['timezone_box', 'UTC'],
        ];
        $out = [];
        foreach ((array) $this->SAMPLE_NAVBAR_BOXES as $name) {
            if (is_string($name) && isset($boxes[$name])) {
                [$method, $selected] = $boxes[$name];
                $out[] = form_item()->$method(['selected' => $selected, 'renderer' => 'div_box']);
            }
        }
        return implode(PHP_EOL, $out);
    }
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) : '';
    }

_sections

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;
    }

_show_for

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>
        ';
    }

_source_text

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'] ?? '') : '';
    }

_subclass

public function _subclass($name)
    public function _subclass($name)
    {
        return _class('sample_' . $name, YF_PATH . '.dev/samples/classes/')->show();
    }