taeluf / liaison
Liaison, for building web apps
Requires
- taeluf/util: v0.1.x-dev
Requires (Dev)
- taeluf/code-scrawl: v0.8.x-dev
- taeluf/tester: v0.3.x-dev
This package is auto-updated.
Last update: 2024-10-10 21:02:34 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 const
s, 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)
- Notes (odd, undocumented, or non-obvious behaviors, mostly.)
- Source Documentation
- Source Code
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 thenull
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
andblog/ListItem.css
files are automatically included in the head html ifListItem.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 usingaddDir()
or when views are in a package'sview/
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
areextract
ed beforerequire
ing 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 optionallyview/theme.css
andview/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.
- Print the
- 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(...)
- Alternative way to call it:
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 tocall_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 forcall_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.
- Hook into
\Lia\Hooks::PACKAGE_ROUTE_PATTERNS_LOADED
, to modify routes discovered within your package - Hook into
"RoutesResolved"
, to modify the content of any matching routes - 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.
- Name your view file with the extension
.md.php
- When loading your view, call it as
namespace:viewname.md
- 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 addonlia:server.view
ob_start()
at the top of a view, thenob_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 ofLia
, 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.