taeluf/liaison

Liaison, for building web apps

v0.6.x-dev 2024-09-02 11:49 UTC

README

Liaison

v0.6 is retired!

v0.7 is the next step for Liaison. I've started adding some new features, better error handling, hooks represented by consts, and there's wayy better documentation right now.

v0.7 hopefully won't bring many breaking changes, but I want the freedom to break some things (such as adding typed params and returns), so that's why I'm bumping the version.

Docs

Liaison is a web-framework focused on building portable web-app libraries, while allowing for fully-functioning web-servers.

The intent is to make any Liaison package easily installable into any website, regardless of the framework at play. Unfortunately, the current version & documentation are mainly geared toward Liaison-powered web-servers.

Documentation Under Development: I just started new documentation on August 22, 2024. I'm working to improve it slowly but surely, simply on an as-needed basis. (i.e. when I personally need to know something, I just write it up in the docs)

Install

composer require taeluf/liaison v0.6.x-dev   

or in your composer.json

{"require":{ "taeluf/liaison": "v0.6.x-dev"}}  

Documentation

Unless linked, the documentation is below.

  • Getting Started
    • Intro
    • Setup Liaison
    • Public Routes
    • Add a theme
  • Packages
    • Directory Structure
    • Bootstrap
  • Views
    • Load Views
    • Add Views
    • Theme / Page Layout
  • CSS & Javascript
  • Routing (aka delivering web page content)
    • Add Routes
    • Deliver Static Files
    • Add Public Directory
  • Hooks (aka events)
  • Markdown Support
    • Public Files / Routes
    • Views
  • Subclassing
    • Packages
    • Addons
  • Notes/Tips/Questions
    • Planned Changes
    • Ideas
  • Other Documentation
    • Generate Liason Documentation
  • Additional Resources (not below)
  • Old Documentation

Getting Started

You need to initialize Liaison, the built-in server package, and your Site's package.

Then you'll need to create public routes & a theme view.

After, you can browse documentation for hooks and other stuff.

To see your site locally, use:
php -S localhost:3000 deliver.php

Intro

Liaison (class Lia) ties together multiple different Packages (class Lia\Package). Packages contain addons (class Lia\Addon), views (php scripts), public files (php scripts), and anything else that is part of your package.

Liaison provides a built-in Server package with addons for routing, hooks, cache, SEO head tags, CSS & Javascript bundling, and views.

I recommend using a main Site package as the base of your site, then create additional packages for any complex features, or features you might want to use on another website later.

Setup Liaison

/deliver.php:

<?php  
require_once(__DIR__.'/vendor/autoload.php');  
  
$lia = new \Lia();    
  
// Add the built-in App, which provides all the web-server features.    
$server_package = new \Lia\Package\Server($lia, $fqn='lia:server');  // dir & base_url not required    
$site_package = new \Lia\Package\Server($lia, 'vendor:namespace', __DIR__.'/Site/');    
  
  
$lia->deliver();    

Public Routes

Files within public/ dir of your package are delivered.

Notes:

  • When .php files are executed and delivered, the extension is replaced with a trailing slash. (i.e. public/bear.php becomes url /bear/)
  • index.php is used for url /
  • non-php files are delivered as-is with appropriate headers, no processing. You can use hooks to modify this behavior (Though I'm not sure this is documented yet).
  • non-php files are delivered with cache headers to cache the file for 30 days.

Sample home page,
public/index.php:

<?php  
// See 'Add Routes' documentation below for a list of variables that are passed to public files.  
?>  
<h1>My first Liaison webpage</h1>  
<p>Woohoo I made it display!</p>  
  
<?php // later, you might display a view here! ?>  
  

Add a theme

getHeadHtml() will print tags for your scripts, css files, seo tags, and some meta tags. $content is passed to the theme view.

Create view/theme.php:

<!DOCTYPE html>  
<html>  
<head>  
    <?=$this->getHeadHtml()?>  
</head>  
<body>  
    <?=$content?>  
</body>  
</html>  

Additional Options:

  • CSS: Create view/theme.css and it is included automatically.
  • Javascript: Create view/theme.js and it is included automatically.
  • Set your theme to a differently named view: $lia->setTheme('namespace:view_name');

Packages

A Liaison package can include addons, routes, views, methods, hooks, and more (or less!).

You can expose functionality for other packages, too.

Directory Structure

Every file & directory is optional

packages/Site/  
    - config.json <- Package settings  
    - bootstrap.php <- is called right after the package is initialized. Use `$this` for your `Package`.  
    - public/ <- Public files to be routed to.   
        - index.php <- home page  
        - contact.php <- /contact/  
    - view/ <- Views  
        - theme.php <- site layout  
        - theme.css <- automatically included when theme is displayed  
    - addon/ <- Addons (extend \Lia\Addon)  
        - MyAddon.php <- (I need to document addons)  
    - class/ <- Classes to autoload, PSR4 style, but you should use composer.  

Bootstrap

If bootstrap.php exists in the root of your package, it is called at the end of Package::__construct()

Example boostrap.php, adding some routes & protecting requests to /admin/ pages within it.:

<?php  
    $package = $this;  
    $lia = $this->lia;  
  
    // Add a directory of additional routes for admin pages  
    $lia->addon('lia:server.router')  
        ->addDirectoryRoutes($package, \My\ClassOfConsts::DIR_ADMIN, $package->url('/admin/'),['.php']);  
        // `DIR_PUBLIC_2` is an absolute path to the directory containing .md and .html public files  
        // `package->url()` returns the target url with the base url of the package prepended. double-slashes are removed.  
        // `['.php']` is an array of extensions to be hidden. i.e. public2/bear.php becomes /bear/  
  
  
    // protect all requests to `admin/*` with a hook, only if this package is being requested.  
    $lia->hook(\Lia\Hooks::ROUTES_FILTERED,  
        function(\Lia\Obj\Route $route) use ($package){  
            if ($route->package() !== $package)return; // we're not filtering for other's packages.  
  
            $admin_url = $package->url("/admin/"); // we must consider the base_url if the package is portable.  
  
            if (substr($route->url(), 0, strlen($admin_url)) == $admin_url){  
                // current_user_is_admin() is NOT part of Liaison.  
                if (!$package->current_user_is_admin()){  
                    // Elsewhere, you might catch \My\Exception and display a message to the user.  
                    throw new \My\Exception(\My\ClassOfConsts::ERR_ADMIN_REQUIRED);  
                }  
            }  
        }  
    );  
  

Views

See the Views Addon.

You should know:

  • Files within the view/ dir of a package are automatically added to Liaison.
  • When a view is added, it goes into the null namespace AND into the given namespace.
  • The namespace is the string preceding the first colon (:) within the view name.
  • View names MUST only contain one colon, or no colons
  • When a view is loaded, it loads from the named namespace, or from the null namespace if none is given. It does NOT fallback to the null namespace if view-not-found in a given namespace.
  • $lia->addon('lia:server.view') to retrieve the view addon.
  • A view named blog/ListItem points to "$view_dir/blog/ListItem.php"
  • blog/ListItem.js and blog/ListItem.css files are automatically included in the head html if ListItem.php is loaded. (and head html is being printed!)
  • Rename an existing view namespace: $lia->addon('lia:server.view')->change_namespace('original', 'new'); (Only changes already-addded views)
  • A package arg is passed to every view IF the view has a package set. (package is set when using addDir() or when views are in a package's view/ directory).
  • (DEPRECATED) theme/ is reserved for views. Don't use it. This will likely be removed in the future.
  • (NOT RECOMMENDED) $lia->addon('lia:server.view')->globalArgs['arg_name'] = mixed $argument;: Add arguments that should be passed to all views. Global args are ignored if a same-named arg is passed directly to a view. (I might remove this feature, or make it package-based.)

Load Views (i.e. display them)

  • $lia->view('namespace:viewname', array $args)
  • or $lia->addon('lia:server.view')->view('namespace:viewname', array $args)
  • File views: $args are extracted before requireing view files.
  • Callable Views: $args is passed as the second argument.

Add Views

  • $lia->addView('namespace:viewname', __DIR__.'/view/'): points to view/viewname.php and its resource files.
  • $lia->addViewCallable('namespace:viewname', function(string $view_name, array $args){})
  • $lia->addon('lia:server.view')->addDir(string $dir_path, \Lia\Package $package). Adds all .php files within as views. $package->name is the namespace.
  • $lia->addon('lia:server.view')->addViewFile('namespace:viewname', string $file_path). Adds a single file as a view.

Theme / Page Layout

Liaison provides a very simple built-in theme to provide a basic HTML structure. You should replace it with your own.

  • Create view/theme.php and optionally view/theme.css and view/theme.js to be your primary page layout.
  • Change the theme to any view with: $lia->setTheme('namespace:viewname') or $lia->addon('lia:server.view')->setTheme('namespace:viewname');
  • Print <head> HTML with:
    • echo $lia->getResourceHtml()
    • or echo $lia->addon('lia:server.resources')->getHtml()

CSS & Javascript

Deliver CSS & Javascript files via:

  • Use the Resources Addon: (The addon determines css vs js based on file extension)
    • Print the <head> html too! (See Theme/PageLayout above)
    • $lia->addon('lia:server.resources')->addFile(string $absolute_file_path): this will be compiled into a mega file. The added file does not need to be accessible via a route.
    • $lia->addon('lia:server.resources')->addUrl('/my-styles.css): This will be added as a <link rel> tag, <script src> for js.
  • Deliver them as static files or place them in the public/ dir, and write the appropriate HTML tag

Routing

The main package contains a Router Addon, which registers global methods on liaison and resolves urls to their targets (file or callable).

Routes can be added through a public/ directory or by calling a method.

Add Routes

  • Place files in the public/ dir of your package
  • Add a single file or callable as a route: $lia->addRoute(addRoute($pattern, $callbackOrFile,$package=null)
    • Alternative way to call it: $lia->addon('lia:server.router')->addRoute(...)
  • Add every file in a directory as a route: $lia->addon('lia:server.router')->addDirectoryRoutes($site_pkg, __DIR__.'/admin-pages/','/admin/',['.php']);

Public File Route: (any file added as a route)

<?php  
/**  
* Several variables are always passed to a public file.   
*   
* - Any variable portions of a pattern (Ex: `/blog/{slug}/` exposes `$slug`)  
*     - If `$slug` is already defined in the current scope, then it will be `$routeslug` instead  
* - $package->public_file_params are `extract`ed and available to your public file.  
*   
* @var Lia\Obj\Route $route The requested route  
* @var Lia\Obj\Response $response The response (*you can modify the headers*)  
* @var Lia $lia  
* @var Lia\Package $package The package your this route belongs to.  
*/  
echo 'hi i\'m tired';  
  

Callable Route:

<?php  
$lia->addRoute('/','hello_world_page'); // pretty sure you can reference functions via a string. I might check later.  
function hello_world_page(\Lia\Obj\Route $route, \Lia\Obj\Response $response){  
    $response->content = "<h1>Hello World</h1><p>This is my first Liaison callable route</p>";  
}  

Deliver Static Files

Use the FastFileRouter to deliver static files and framework-free scripts quickly and easily. (Static files can also be delivered via the public/ dir or addRoute())

In your deliver script, after autoload, before any liaison setup:

<?php  
// require(__DIR__.'/vendor/taeluf/liaison/code/class/Router/FastFileRouter.php'); ## less cpu work than autoloading  
  
// `extract($args)` and `require` a php file and `exit`. Route must end with a trailing slash and not `.php`. Does nothing if file not found.   
\Lia\FastFileRouter::php(__DIR__.'/fastroute/',$args=[]);  
  
// Deliver static file contents and `exit`. Does nothing if REQUEST_URI ends with `.php` or if file is not found.  
// You MAY pass string url as the 2nd param.  
\Lia\FastFileRouter::file(__DIR__.'/file/');  
  
// Manually deliver a file with appropriate header information (uses browser cache!)  
\Lia\FastFileRouter::send_file(__DIR__.'/path-to-file.css');  

php() and file() prevent path traversal by removing all occurences of ...

Add Public Directory

These routes are thus handled by Liaison. (The Server package class calls hooks when setting up public routes, but this example does not.)

<?php  
$public_dir = __DIR__.'/src/public-files/';  
$patterns = $lia->fqn_addons['lia:server.router']  
                 ->dir_to_patterns($public_dir);  
  
$base_url = '/my-custom-blog/';   
  
$package = $lia->fqn_packages['lia:server']; // or your own package, referencing the FQN passed to the Package constructor  
  
foreach ($patterns as $file=>$pattern){  
    // Double-slashes are removed from routes. I.e. it's okay if you accidentally leave a double slash  
    $full_pattern = $base_url.$pattern;  
  
    // package is optional!  
    $lia->addRoute($full_pattern,$public_dir.'/'.$file, $package);  
}  

Hooks

The main package contains a Hooks Addon, and it registers methods on Liaison.

Hooks have no forced naming shceme. Both namespace:package.hook_name and const string \Namespace\Hooks::HOOK_NAME are optional suggestions, class const recommended.

Add Hooks: (i.e. your code will be called)

  • $lia->hook('namespace:package.hook_name', [$object, 'method']); ... method receives whatever args are passed to call_hook()
  • or $lia->addon('lia:server.hook')->add(\Namespace\Hooks::HOOK_NAME, [$object, 'method']);

Call Hooks: (i.e. trigger other people's code)

  • $lia->call_hook('namespace:package.hook_name', $arg1, $arg2, $arg3). Returns an array of return values of each hook called.
  • $lia->$lia->addon('lia:server.hook')->call(\Namespace\Hooks::HOOK_NAME, ...$args);

Existing Hooks: (TODO)

  • See Hooks
  • Other hooks are badly organized:
    • See code/class/Hooks.php for new hooks
    • I think some are in code/class/Other/PackageLifeCycleInterface.php
    • Various hooks are scattered throughout the code/ directory. Try grepping for call_hook( or ->call(

Markdown Support

Markdown support is not built in, but you can set it up using hooks. (Other approaches are also possible, but this is my recommendation.)

For these examples, I use CommonMark markdown to html converter. (cm github).

Public Files / Routes

Markdown support can be added to public route files with the following steps & 2 code snippets.

  1. Hook into \Lia\Hooks::PACKAGE_ROUTE_PATTERNS_LOADED, to modify routes discovered within your package
  2. Hook into "RoutesResolved", to modify the content of any matching routes
  3. Install CommonMark or an alternative. (links above)

In your deliver script, BEFORE adding your package that has markdown routes, add a hook to modify patterns and remove .md extension from urls.

<?php  
$lia->hook(\Lia\Hooks::PACKAGE_ROUTE_PATTERNS_LOADED,  
    function(Lia\Package\Server $package, $array_of_patterns): array {  
        //if ($package->fqn != 'reedybear:site')return $array_of_patterns;  
  
        foreach ($array_of_patterns as $rel_file=>$url_pattern){  
            if (substr($url_pattern,-4)=='.md/'){  
                $new_pattern = substr($url_pattern,0,-4).'/';  
                if (substr($new_pattern,-7) == '/index/')$new_pattern = substr($new_pattern,0,-6); // keep the end slash  
                $array_of_patterns[$rel_file] = $new_pattern;  
            }  
        }  
        return $array_of_patterns;  
    }  
);  

Then hook into the hook RouteResolved to convert the route's output to markdown:

<?php  
  
$lia->hook('RouteResolved',  
    function(\Lia\Obj\Route $route, \Lia\Obj\Response $response){  
        if (!$route->isFile())return;   
        $target = $route->target();  
        if (substr($target, -7) != '.md.php'  
            && substr($target, -3) != '.md'  
        ){  
            return;  
        }  
  
        $converter = new \League\CommonMark\CommonMarkConverter([  
            'html_input' => 'allow', // https://commonmark.thephpleague.com/2.5/configuration/ ## allow, strip, or escape  
            'allow_unsafe_links' => true, // DANGEROUS  
        ]);  
  
        $response->content =  
            $converter->convert($response->content);  
    }  
);  

Views

Markdown support can be added to views with a single hook & file name change.

  1. Name your view file with the extension .md.php
  2. When loading your view, call it as namespace:viewname.md
  3. Use the VIEW_LOADED hook to convert any view ending with .md into html.
<?php  
$lia->hook(\Lia\Hooks::VIEW_LOADED,  
    function(?string $namespace, string $view_name, mixed $view, string $content): string {  
        if (substr($view_name,-3) != '.md')return $content;  
        // alternatively, you could just convert every view within your namespace, or consider other schemes.  
  
        $converter = new \League\CommonMark\CommonMarkConverter([  
            'html_input' => 'allow', // https://commonmark.thephpleague.com/2.5/configuration/ ## allow, strip, or escape  
            'allow_unsafe_links' => true, // DANGEROUS  
        ]);  
  
        return $converter->convert($content);  
    }  
);  

Alternate approaches:

  • Add views as callables, then your callable can do the conversion.
  • Replace the view method on Liaison or the addon lia:server.view
  • ob_start() at the top of a view, then ob_get_clean() and convert to HTML and echo at the end.

Subclassing

See Source Documentation for class details, or Source Code for poorly documented features.

Packages

A custom Package class can significantly ease setup of your package by pre-setting the directory and namespace.

Minimal Example:

<?php  
  
namespace ReedyBear\PageCreator;  
  
class Package extends \Lia\Package\Server {  
  
    public string $fqn = 'reedybear:page_creator';  
    public string $name = 'page_creator';  
  
    public function __construct(\Lia $lia, ?string $base_url=null){  
        parent::__construct($lia, $this->fqn, \ReedyBear\PageCreator::DIR_PACKAGE, $base_url);  
    }  
  
}  

Addons

@TODO write Addons subclassing documentation

Notes/Tips/Questions

See Notes for various undocumented functionality.

Planned Changes

These changes will take place piece-by-piece over time. I plan no major overhauls in a single update.

  • Hooks: Many hooks defined within this repo are strings like 'RequestStarted' and they're hardcoded where the hook is called. I'm slowly-but-surely converting these to class consts, like \Lia\Hooks::REQUEST_STARTED = 'lia:server.request_started'.
  • Types: Much of Liaison does not have types hardcoded. This was a 'feature' previously, but I hate it. I'm adding hardcoded types slowy-but-surely.
  • Methods: I've considered removing global methods and adding some new methods to Lia itself. I've also considered creating a compiled version of Lia, but I don't think I'll ever do that. Calling methods directly may break at some point, in favor of load-addon, call addon.

Ideas

  • $lia->depend('ns:package) or $lia->depend('ns:package.addon') as a way to formalize dependencies, throwing an exception if the dependency is not present. Might be nicer than the if has approach.