The freia library is a tome of php server magic that you can add to any project, be it an exotic blank page, that awesome box of horrors you unwillingly inherited from your dev grandma' (which you're too scared to touch), even that web framework that's the fastest "hello world" claptrap of the month.
Unlike other solutions even if you don't pick freia to start a project, or don't pick freia for your main logic, we've designed freia to be a solution you can always goto. It's never too late to use freia.
Among the solutions we provide in the main modules, you'll find easy routing
though HttpDispatcher
, programatic
security and access control via Auth
and
protocols, an easy to use cli system via
Console
, flexible migration system via
the pdx
command, and much more...
The code is clean and easy to read, verifiable, self contained and replacable.
The project is hosted on GitHub.
You can report bugs and discuss features on the issue pages. Feel free to also
send tweets on twitter using the hashtag
#freialib
, or post questions on
stackoverflow using the
freialib
tag.
The freia library is distributed under a BSD 2-clause license.
See License section for more
information.
If you have an existing codebase the way you include freia is hugely dependent on your circumstances. We recomend reading the freia Module docs. After you understand the requirements for autoloading and initialization you should have a good idea what you need to do in your project to integrate freia.
The recomended method to get the freia library is via
composer. We recomend keeping the
composer.lock
file and also commiting all the files to your
version control system rather then rely on re-downloading each time. It is much
more reliable to rely on the servers your code base is on then several servers
all over the internet, any of which can go down and break your project.
It is prefered to not rely on composer on the server side, which isn't a problem even if you want to clone to your server if you simply commit all your dependencies into your revision control system.
$ mkdir PROJECT/
$ cd PROJECT/
$ edit composer.json
{
"require": {
"freia/autoloader": "1.*",
"hlin/archetype": "1.*",
"hlin/attribute": "1.*",
"hlin/tools": "1.*"
}
}
$ composer install
Add paths to load
(ie. your own paths) to have them recognized
by freia's autoloader.
All out coding hermit mode,
{
"extra": {
"freia": {
"load": [ "vendor" ]
}
},
"require": {
"freia/autoloader": "1.*"
}
}
The bundle module is completely empty and won't get picked up by freia's autoloader, it just requires all the official modules in the freia library.
For a list of bundled in modules please refer to the freialib-bundle/composer.json
{
"extra": {
"freia": {
"load": [ "vendor" ]
}
},
"require": {
"freialib/bundle": "1.*"
}
}
As before add paths to load
(ie. your own paths) to have them
recognized by freia's autoloader.
Just in case you want to customize rather then build up
your composer.json
.
{
"name": "Awesome",
"version": "1.0.0",
"authors": [
{
"name": "Awesome",
"role": "Developer"
}
],
"extra": {
"freia": {
"cache.dir": "files/cache",
"load": [
"src/server/system",
"src/server/theme",
"packagist"
]
}
},
"config": {
"preferred-install": "dist",
"vendor-dir": "packagist",
"bin-dir": "packagist/.bin",
"cache-files-ttl": 0
},
"require": {
"php": ">=5.4.10",
"freialib/bundle": "1.*"
}
}
The above is used as-is in the example application. See next section for more details.
We provide a basic framework if you start from a completely blank slate to help you get going. We do recomend people make their own project structure from scratch to get the most benefit by avoiding any unncesary clutter that a generic structure might impose, but if you're in a hurry or need a reference point here you go:
$ git clone --depth=1 https://github.com/freialib/framework YOUR_PROJECT/
$ cd YOUR_PROJECT/
$ rm -rf .git
$ cd 1.x/
$ git init
$ git add -A
$ git commit -m "1.0"
Above 1.0
and 1.x
refer to your own project; not freia.
If you don't have the git
please go to
http://git-scm.com/, it's a great tool
The freia library is distributed using a BSD 2-clause license. You can find the license text in the license.txt file. The BSD 2-clause license allows you to use freia for both open source and commercial projects with little to no restrictions; mainly you just need to maintain the license text in the files, even if you make modifications to the files in question.
This document is the entire freia documentation, the official documention, and the main and only format of the docs.
We are intentionally avoiding a printable and automatically generated versions due to the complexity that introduces both to maintaining the docs, writing the docs, contributing to the docs, the organization of the docs, as well as the complexity it adds to the code. We don't believe extremly long blocks of documentation in code to be all that useful to inline auto-complete.
If you wish to correct or add anything to this document simply go to the
file
of this document on the site's github repo and press the Edit
button. You will need a free Github account.
Github will magically create a copy of the repo and file (fork), and allow you
put your changes into a patch which you can send back to us to integrate (pull
request).
The documentation is organized into topics and modules. Modules have have concepts and classes organized in sub-namespaces. Sub-namespaces may be organized into categories when relevant (the category will be in paranthesis).
subnamespace
subnamespace (category)
We try to conver everything, but we do not try to cover everything systematically. This may seem strange, but we have a lot of details in the way things are implemented, convering things one by one is actually a lot more confusing then presenting the concept, ie. the problem space it was designed to solve.
In this document we use universal namespace notation to refer both classes
and namespaces and combinations there of. As an example
\hlin\archetype\Context
is written as
hlin.archetype.Context
which is also the format that's used and
recomended for configuration files since it avoids confusing escaping of the
\
character.
freia
ModuleThe freia module consists of only the main freia
namespace. To
allow for working with composer the module is written in a
PSR-4 format.
All other modules in the library are defined as
freia-cfs-module
s.
The namespace actually only really contains one class
freia.SymbolLoader
that's independent, the other utility classes
contained within, outside of freia.Panic
which is a conventions
requirement (see: exceptions & errors),
actually require the autoloader to be active for the dependencies to resolve
(mainly interface dependencies).
In the following we'll be using basing code on the structure "complete"
composer.json
example in the Installing
chapter. To avoid confusion we'll quickly go though the basic scaffold you may
need to build a project to easily follow the examples while writing them out,
should you wish to. Feel free to diverge from waht we're about to show.
$ mkdir PROJECT/
$ cd PROJECT/
$ mkdir -p src public files/cache files/logs
$ edit composer.json
edit
is whatever editor meets your fancy; we recomend using a
universal one.
{
"name": "Awesome",
"version": "1.0.0",
"authors": [ { "name": "Awesome", "role": "Developer" } ],
"extra": {
"freia": {
"cache.dir": "files/cache",
"load": [ "packagist", "src" ]
}
},
"config": {
"preferred-install": "dist",
"vendor-dir": "packagist",
"bin-dir": "packagist/.bin",
"cache-files-ttl": 0
},
"require": {
"php": ">=5.4.10",
"freialib/bundle": "1.*"
}
}
$ composer install
$ edit public/index.php
<?php namespace appname\main;
error_reporting(-1);
// public directory
$wwwpath = realpath(__DIR__);
// system path; the root of the file system context
// it's essentially the virtual equivalent of unix /
// ideally there would never be access outside the syspath
$syspath = realpath("$wwwpath/..");
// the context in which "your" source files can can be easiest
// accessed from; generally this is where your main source files is
$srcpath = realpath("$syspath/src");
// instead of declaring the above as special variables we simply
// include the context of the main file in this context and gain
// access to them we can always dismiss them via unset($varname)
include "$srcpath/main.php";
$ edit src/main.php
You can start a server by opening a console and running,
$ cd public
$ php -S localhost:3000
fenrir.SymbolLoader
is the class you should be looking when
trying to initialized the autoloader. The class fenrir.Autoloader
is intended for use in contexts where the archetype is required. You can not
initialize it with out having fenrir.SymbolLoader
already
initialized.
Simple autoloader initialization:
<?php namespace appname\main;
/**
* @return \hlin\archetype\Autoloader
*/
function autoloader($syspath) {
// include composer autoloader
require "$syspath/packagist/autoload.php";
// paths where to search for freia modules (relative to $syspath)
$paths = ['src', 'packagist'];
// initialize
$autoloader = \freia\autoloader\SymbolLoader::instance (
$syspath, [ 'load' => $paths ] );
// add as main autoloader
$isRegistered = $autoloader->register(true);
$isRegistered or die('Failed to register as main autoloader');
// fulfill archetype contract before returning
return \freia\Autoloader::wrap($autoloader);
}
// ...
autoloader($syspath);
We recomend storing the paths configuration in your
composer.json
. Building on the above example you can just change
$paths
as follows to configure modules via your
composer.json
configuration:
<?php namespace appname\main;
/**
* @return \hlin\archetype\Autoloader
*/
function autoloader($syspath) {
// include composer autoloader
require "$syspath/packagist/autoload.php";
// read environment from composer.json
$composerjson = "$syspath/composer.json";
file_exists($composerjson) or die(" Err: missing composer.json\n");
$env = json_decode(file_get_contents($composerjson), true);
// initialize
$autoloader = \freia\autoloader\SymbolLoader::instance (
$syspath, $env['extra']['freia'] );
// add as main autoloader
$isRegistered = $autoloader->register(true);
$isRegistered or die('Failed to register as main autoloader');
// fulfill archetype contract before returning
return \freia\Autoloader::wrap($autoloader);
}
Classes written in freia modules follow the following conventions:
next\<namespace>
the
class will resolve to the first class in the namespace after the
current module in the stack that has the class in the namespace after
next
as it's main namespaceGoing back briefly to the second rule, a class ex.ExampleClass
will match ex.system.ExampleClass
as well as
ex.system.core.ExampleClass
; but it will also match
myns.ex.ExampleClass
as well as in extreme examples
myns.core.system.ex.overwrites.ExampleClass
. You dont have to
worry about the system becoming this rediculas in practice but given the rules
do allow that much flexibility please make sure to pick a very unique main
segment to your namespace.
As an optional requirement, which we do not impose, we recomend namespaces
be lowercase letters (using \
to seperate words) and not be used
as an extention of class names (eg. appname.Controllers.Home
would
be incorect under the recomendation). Please keep all the information within
the class name and use the namespace purely as a "name workspace."
With regard to class names being case sensitive, in part this is a restriction inherited from compatibility with composer, but that aside in practice we find it's fairly easy and desirable to keep classes case sensitive. Code is more consistent and as a bonus file names of classes using camel case are readable, as opposed to the being a amalgam of lowercase words which would happen if we had made it case insensitive.
// Controller_Home.php
class Controller_Home
// Controller/Home.php
class HomeController
// Command/Example.php
class ExampleCommand
// Trait/Example.php
class ExampleTrait
// Signature/Example.php
interface ExampleSignature
// Database/ReallyLongName.php
class ReallyLongNameDatabase
We'll assume the following stack of modules and classes. Remember you can use the command line helpers to diagnose the class resolution if you need it; in practice it's rarely as complex as the example bellow.
module0.module2
class Example1
module1.system.legacysupport
class Example1
module1.system.core
class Example1
class Example2 extends next\module1\Example1
module1.tools
class Example1
module2.system
class Example1
module3.system
class Example1
class Example3 extends \module1\system\core\Example1
In the examples bellow A <- B
means A
extends B
# simple resolution (absolute and dynamic)
# ------------------------------------------------------------------------
\module1\system\core\Example1 // => module1.system.core.Example1
\module1\system\Example1 // => module1.system.legacysupport.Example1
\module1\tools\Example1 // => module1.tools.Example1
\module1\Example1 // => module1.system.legacysupport.Example1
# overwriting from foreign namespace
# ------------------------------------------------------------------------
\module2\system\Example1 // => module2.system.Example1
\module2\Example1 // => module0.module2.Example1
# infinite blind extention via the "next" keyword
# ------------------------------------------------------------------------
\module1\Example2 // => module1.system.Example2 <- module1.tools.Example1
# explicit inheritance
# ------------------------------------------------------------------------
\module3\Example3 // => module3.system.Example3 <- module1.system.core.Example1
Settings up the autoloader allows you to use modules, however in freia most modules require a context to work with. Things such as logs, file system functions, cli functions, web functions, paths are all part of the context passed into classes that require them.
Here is a minimalistic initialization block:
<?php namespace appname\main;
use \hlin\archetype\Autoloader;
/**
* @return \hlin\archetype\Context
*/
function context($syspath, $logspath, Autoloader $autoloader) {
// php settings
date_default_timezone_set('Europe/London');
// filesystem wrapper
$fs = \fenrir\Filesystem::instance();
// logger setup
$secpaths = ['syspath' => $syspath]; // paths to obscure
$logger = \hlin\FileLogger::instance($fs, $logspath, $secpaths);
// configuration reader
$filemap = \freia\Filemap::instance($autoloader);
$configs = \freia\Configs::instance($fs, [ $filemap ]);
// main context
$context = \fenrir\Context::instance($fs, $logger, $configs);
// paths
$context->addpath('syspath', $syspath);
$context->addpath('logspath', $logspath);
$context->addpath('cachepath', realpath("$syspath/cache"));
return $context;
}
// ...
$logspath = "$syspath/files/logs";
$context = context($syspath, $logspath, $autoloader);
99% of modules will require just the above to function. In some cases you may need to add to the above to satisfy the special needs of specialized modules; be it extra path constants or just diffrent classes for the initialization altogheter.
Here are a few "extra" configuration values as an example:
<?php namespace appname\main;
/**
* @return \hlin\archetype\Context
*/
function context($syspath, $logspath, Autoloader $autoloader) {
// ...
// versions
$pkg = json_decode(file_get_contents("$syspath/composer.json"), true);
$mainauthor = $pkg['authors'][0]['name'];
$context->addversion($pkg['name'], $pkg['version'], $mainauthor);
$context->setmainversion($pkg['name']);
$context->addversion('PHP', phpversion(), 'The PHP Group');
// special handlers
$context->filemap_is($filemap);
$context->autoloader_is($autoloader);
return $context;
}
So far we've gone into what code you need for initialization but not really
how you use it. Let's assume the appname.main.context
function
defined above is in context.php
and the code illustrated
in the Autoloader section is in autoloader.php
.
Hint: move them there if you're coding along with the docs.
We recomend you always use main
for the filename of your entry
point, and place your entry point on the root of the project.
Assuming all files mentioned so far are on the root of your project, here's a minimal example of how your entry point might look for a web app:
<?php namespace appname\main;
use \fenrir\MysqlDatabasel;
use \fenrir\HttpDispatcher;
/**
* ...
*/
function main($syspath, $srcpath, $wwwconf) {
$logspath = realpath("$syspath/files/logs");
require "$srcpath/autoloader.php";
require "$srcpath/context.php";
// init autoloader
$autoloader = autoloader($syspath);
if ($autoloader === null) {
error_log("Failed loading autoloader.");
return 500;
}
// init main context
$context = context($syspath, $logspath, $autoloader);
try {
// Example Main Logic
// ------------------
$dbconf = $context->confs->read('freia/databases');
$mysql = MysqlDatabase::instance($dbconf['appname.mysql']);
$http = HttpDispatcher::instance($context);
require "$syspath/routes/main.php"; # => router function
\appname\routes\router($http, $context, $mysql);
return 0; # success
}
catch (\Exception $exception) {
$context->logger->logexception($exception);
return 500;
}
}
<?php namespace appname\main;
$wwwpath = realpath(__DIR__);
$syspath = realpath("$wwwpath/..");
$srcpath = realpath("$syspath/src");
// ignore existing files in PHP build-in server
$uri = $_SERVER['REQUEST_URI'];
if (strpos($uri, '..') == false) {
$req = "$wwwpath/$uri";
if (file_exists($req) && is_file($req)) {
return false;
}
}
// private keys, server settings and sensitive information
$wwwconf = include "SECURE/PATH/TO/wwwconf.php";
$wwwconf['wwwpath'] = $wwwpath;
require "$srcpath/main.php";
// invoke main
$exitcode = main($syspath, $srcpath, $wwwconf);
// handle system failure
if ($exitcode != 0) {
$errpage = "$wwwpath/$exitcode.html";
if (file_exists($errpage)) {
http_response_code($exitcode);
include $errpage;
}
}
To setup a specialized entry point it's fairly easy:
#!/usr/bin/env php
<?php namespace appname\main;
date_default_timezone_set('Europe/London');
$syspath = realpath(__DIR__);
$srcpath = realpath("$syspath/src");
$logspath = realpath("$syspath/files/logs");
require "$srcpath/autoloader.php";
require "$srcpath/context.php";
// init autoloader
$autoloader = autoloader($syspath);
$autoloader !== null or die(" Err: Missing dependencies.\n");
// init context
$context = context($syspath, $logspath, $autoloader);
// create console
$console = \hlin\Console::instance($context);
// invoke main
$commands = $context->confs->read('freia/commands');
exit($console->main($commands));
A freia module is defined by prividing a namespace (we recomend always
providing a subnamespace as well) and setting the type to
freia-module
in the composer.json
of the module.
{
"name": "yournamespace/subnamespace",
"type": "freia-module"
}
The above is the minimum requirement and will work fine if you maintain the module only in your version control but if you wish to create a distributable module you'll need to add a few more fields to the file, please refer to the composer schema documentation for all requirements.
There is no mandatory directories in the module structure. Of the directories
freia only requires src
and confs
; anything else
is considered specialized functionality (this includes tests). Here is a module
example:
your_module/
confs/
src/
composer.json
Ocassionally you'll need to have modules that are loaded in a specific order, or follow certain rules, on such module type are debug modules. Debug modules are only loaded if in debug mode, otherwise they are ignored. To specify a debug module specify it in the freia rules section:
{
"name": "yournamespace/subnamespace",
"type": "freia-module",
"extra": {
"freia": {
"rules": {
"identity": [ "debug" ],
}
}
},
}
If you just need to specify a module should precede another module use
the matches-before
rule:
{
"name": "yournamespace/subnamespace",
"type": "freia-module",
"extra": {
"freia": {
"rules": {
"matches-before": [
"hlin.security",
"hlin.archetype",
"hlin.attribute",
"hlin.tools"
]
}
}
},
}
As in the example you can specify multiple other modules. You may also use
both the debug
module identity
and the
matches-before
rule simultaniously on a single module.
hlin
ModuleThe hlin
module specialises in portable and completely testable
code; unlike the fenrir
module which
specialises on classes that require deep coupling.
Interfaces in freia are refered to as "interfaces" only in the writing of the implementation, in all other cases interfaces fall under one of three categories:
archetypes
— concepts intended to be either re-used
a lot, or otherwise very useful to standardize to ensure implementations
that make use of them are easier to conceptualize by conceptualizing the
sum of its parts rather then the whole. In almost all cases, archetypes are
located in a specialized namespace, usually
<module>/archetype
. Archtypes ALWAYS provide traits, and
it MANDATORY to use the main trait given, since it allows for dynamic
extention. They also sometimes provide Mocks
, and
sometimes provide TestTraits
; in other words all the
little building blocks you would need for speedy implementation.attributes
— a specialized light weight form of
archetypes, in general used to denote a class accepts a certain archetype
or otherwise has some very minor enhancement to functionality, eg.
Contextual
, Configurable
, etc. Attributes also
may provide traits but the traits are not mandatory, unlike archetype
traits.signatures
— are purely method signatures, they
serve only to allow different implementations to avoid extending the
initial class which is painful most of the time (using adaptors is much
more reliable and clean). Signatures are generally just a interface with
the ClassName
followed by a Signature
suffix,
eg. ResponseSignature
, FlagparserSignature
,
MysqlDatabseSignature
, etc. Signatures generally dont have
traits or the traits are not directly tied to the signatures in name.There are of course some minor "DO NOTs"
And of course a few optional usage recomendations:
<?php namespace yourmodule\system;
use \yourmodule\archetype\Example;
use \hlin\attribute\Contextual;
use \fenrir\tools\ResponseSignature;
class SimpleExample implements Contextual, Example, ResponseSignature {
use \hlin\ContextualTrait;
use \yourmodule\ExampleTrait;
// ...
} # class
<?php namespace yourmodule\archetype;
interface Example
<?php namespace yourmodule\attribute;
interface Example
<?php namespace yourmodule\system;
interface ExampleSignature
To make all classes easy to test, self container, including classes in other
modules not just hlin
modules, we employ contexts. What contexts
do is isolate actions that would have consequences via proxy methods.
In freia you'll generally have to deal with the Filesystem context, CLI context, Web context, and so on. Since dealing with them individually is such a giant pain, and generally tends to just take too much implementation time, we recomend you deal with them via the all-encompasing master Context class and Contextual attribute, which provide you with a single object that contains all the contexts you'll ever need for most cases.
Classes that use just the master context are also fairly easy to work with, since the master context object is easy to work with in general.
$console = \fenrir\Console::instance($context);
$commands = $context->confs->read('freia/commands');
return $console->main($commands);
We believe in the value of terse code. Easier to read is easier to maintain.
In freia the use SomethingException
is discouraged, largely
because of the confusion the notion of Exception
has drawn
near it.
An exception in freia is defined as literally a more elegant
die('application logic failed horribly')
any other use is
considered purely bad design. To avoid confusion exception classes are never
named Exception but instead Panic. This makes their use more intuitive
as well. Every module/namespace is should create it's own Panic class, ideally
when throwing a Panic there should be no need for a namespace qualifier since
the namespace in question has a Panic class.
When errors that can or "should" be handled happen, ie. errors that are not just breaking internal assumtions, then those errors should be handled though error codes. If you do not have a clear set of error codes already merely use http codes, with 0 as no error (if you wish to use 200 you can have it as no error and did something; particularly if you have a function that can return literally anything). As far as passing the error codes we recomend the multi-return pattern.
In most cases you just need two codes: success and error, and for most cases "success" = 0, "error" = anything else (we recomend 500).
Somehow leveraging exceptions may seem "more convenient" but it is actually an anti-pattern as it leads to lazily handling everything globally, ie. not handling it, just assuming failure. You should have global handlers, however you should strive to never have them called, since generically handling an error is always far more poor implementation then handling it closest to it's location. Handling though exceptions also has the issue of leading to ambigous error handling: if X enters an error state, who is responsible for handling it? the caller might not be, the caller's caller might not be either, we can assume the global error handler will catch it and handle it but it can very well be caught anywhere between; there's also no responsibility of knowing it can happen much less mitigating it. By handling the error as a return you ensure the error propagation is clear and every step takes responsibility leading to much more sane and predictable system as a whole.
Using null
as "no result marker."
// no result, success
return [null, 0];
// no result, success
return [$res, 0];
// no result, failure
return [null, 500];
// failure with message
return ["error message", 500];
// no result failure with complex message
return [$errorObject, 500];
// different failure
return [$error, 404];
// guess what failure this is...
return [$error, 401];
// how about this one?
return [$error, 400];
// get result and error
list($res, $err) = $example->mymethod();
// get only error
list(, $err) = $example->mymethod();
Slight variation of above, allows for null
as valid result.
// success (null here is just a placeholder, any value can be used)
return [null, 0];
// we got a result (it's null), success
return [null, 200];
// etc
No return value? You should still use the same pattern and return
null
. By doing this you can always add a value and there won't be
any change required to existing code since the pattern will just naturally flow
into pattern I and II.
// success
return [null, 0];
// failure
return [$error, 500];
The prefered way to handle security in freia is though an authorizer
object. An authorizer allows you to create a programatic access control system
though the use of Protocol
classes. The system is, as mentioned,
purely programatic, so if you want/need a ACLs you'll just have to hook into
it and make a protocol that fits your needs. In most cases you don't have to,
since unless your users need to be able to dynamically change permissions
it's much much simpler to just have a static configuration for the entire
system. It's also, much faster.
The system is primarily designed with the following goals in mind:
To get an authorizer object you just need a list of protocols that allow a an action, a list of protocols that explicitly forbid an action, a list of aliases for roles in the system to help you organize and avoid repeating yourself (ie. "an admin is also a member, and therefore has the access rights a member has, by consequence") the id of the current user, and the role of the current user.
$whitelist = include __DIR__."/whitelist.php";
$blacklist = include __DIR__."/blacklist.php";
$aliaslist = include __DIR__."/aliaslist.php";
// if the current user is logged in
$auth = \hlin\Auth::instance
(
$whitelist, $blacklist, $aliaslist,
$user_id, $role
);
// ...
// guest user
$auth = \hlin\Auth::instance($whitelist, $blacklist, $aliaslist);
To create protocols simply extend hlin.Protocol
or use the
hlin.Check
helper.
Here is a very simple example of the lists, note that only the whitelist is actually implicitly mandatory, so you can just pass empty arrays for the blacklist and aliaslist.
<?php namespace hlin; return [
// you can use the roles as keys
'admin' => [
Check::entities([ 'admin_area' ])->unrestricted(),
],
// the guest role is specified though a constant Guest
Auth::Guest => [
Check::entities([ 'login' ])->unrestricted(),
],
// or create tag roles (see aliaslist for usefulness)
'+members' => [
Check::entities([ 'access_site' ])->unrestricted(),
],
]; # conf
<?php namespace hlin; return [
// due to this rule guests can't "access_site" regardless of what
// the whitelist specifies; blacklists are absolute
Auth::Guest => [
Check::entities([ 'access_site' ])->unrestricted(),
],
// tag roles don't work here; blacklists don't invoke aliases, you must
// explicitly blacklist a role, inheriting from a role doesn't imply
// you inherit it's blacklist, only it's whitelisting
]; # conf
<?php namespace hlin; return [
'member' => [ '+members' ],
'admin' => [ '+members' ],
]; # conf
A simple console helper is provided by default. To create a basic
console
script you would write the following code,
minimal example:
#!/usr/bin/env php
<?php namespace appname\main;
date_default_timezone_set('Europe/London');
$syspath = realpath(__DIR__);
$srcpath = realpath("$syspath/src");
$logspath = realpath("$syspath/files/logs");
require "$srcpath/autoloader.php";
require "$srcpath/context.php";
// init autoloader
$autoloader = autoloader($syspath);
$autoloader !== null or die(" Err: Missing dependencies.\n");
// init context
$context = context($syspath, $logspath, $autoloader);
// create console
$console = \hlin\Console::instance($context);
// invoke main
$commands = $context->confs->read('freia/commands');
exit($console->main($commands));
$ chmod +x console
Invoking from the command line is fairly simple.
$ ./console
It's important to note that by default the library does not provide any sophisticated flag parsing; it's a lot more flexible that way and command parameters tend to be both simpler and easier to implement.
To create a command you simply need to do two things:
hlin.archetype.Command
Writing the class should be fairly straight forward. You can have any name and place it anywhere but we do recomend ending with the Command suffix if at all possible.
<?php namespace your_namespace\tools;
class ExampleCommand implements \hlin\archetype\Command {
use \hlin\CommandTrait;
/**
* @return int
*/
function main(array $args = null) {
// ...
return 0; # success
}
} # class
There is no convention for where to place the configuration for commands,
but obviously if you place it somewhere else you'll have to specify it, merge
it, convert it, etc to get to the $commands
variable we got to
in the previous section. If you just wish to keep it simple and stupid just
place it under a freia/commands
configuration file in ANY module
of your choice; you may create a file if the module doesn't have it.
<?php return [
'make' => [
'topic' => 'fenrir.tools',
'command' => 'fenrir.MakeCommand',
'flagparser' => 'hlin.NoopFlagparser',
'summary' => 'make a class, or just about anything else',
'desc' =>
"The make command will try to figure out what you want to make and create it as apropriatly as possible. ".
"What you pass to it is considered the 'problem' and is what the command is trying to solve. A solution will be presented before executing it.\n\n".
"If it's a class it will try to place it in the right namespace, under the right directory structure and have it implement the right interfaces.\n\n".
"You can skip the guessing by providing a domain to the problem using the domain:problem syntax. Please see examples for details. ".
"Some patterns may always require the domain to work.",
'examples' => [
'?' => "Get available domains",
'example.FooCommand' => "Create class FooCommand (that implements \hlin\archetype\Command) with namespace example; place it in the file /Command/Foo.php located in the class path for the module specific to namespace example",
'class:example.FooCommand' => 'Same as above only we ensure that the problem example.FooCommand is interpreted as a class'
],
'help' =>
" :invokation ?\n\n".
" Get all supported problem domains.\n\n".
" :invokation [<domain>:]<problem>\n\n".
" Solve the given problem. Domain is optional.\n".
" If domain is not provided the command will try to guess."
],
]; # conf
<?php return [
'make' => [
'topic' => 'fenrir.tools',
'command' => 'fenrir.MakeCommand',
'summary' => 'make a class, or just about anything else',
'examples' => [
'?' => "Get available domains",
'example.FooCommand' => "Create class FooCommand (that implements \hlin\archetype\Command) with namespace example; place it in the file /Command/Foo.php located in the class path for the module specific to namespace example",
'class:example.FooCommand' => 'Same as above only we ensure that the problem example.FooCommand is interpreted as a class'
],
],
]; # conf
Help commands are fairly straight forward. We'll assume
./console
is your console script, to get general help you just
invoke:
$ ./console help
# or just...
$ ./console
# or...
$ ./console --help
You can filter the commands you see by category; the commands with out category are refered to as "application commands" and have "application" as the category.
# all commands
$ ./console help
# only application commands
$ ./console help application
# only fenrir.tools commands
$ ./console help fenrir.tools
To get help on a command simply use the ?
command:
$ ./console ? help
$ ./console ? pdx
$ ./console ? make
The HoneypotCommand
allows you to generate autocomplete files
IDEs can use to "get the correct idea" of what extends what. This both helps
in providing accurate autocomplete information as well as avoiding annoying
"this class must be declared abstract" false positives.
# regenerate all honeypots
$ ./console honeypot
Some IDEs (notably netbeans) are slow to process. You may need to both wait a few seconds and/or open the honeypot files.
If you have improper implementation honeypot generation will throw errors on the console; please fix those errors; there is no bypassing. Generally these are 99% of the time: missing method implementation on non-abstract classes, bad class declarations, etc. You can think of honeypots as a light mini-linter since they force all your files though the pipe.
hlin.archetype.Context
A context is an archetype that's used to encapsulate system classes, such as a Filesystem, Configs handlers, Web handlers, etc. To facilitate access the traits for the archetype used public attributes. The archetype doesn't contain much outside of setters and getters to system classes.
The class also serves to maintain paths, versions and other misc functionality.
You can easily create classes that accept a context via the Contextual attribute.
$context->confs // => \hlin\archetype\Configs
$context->cli // => \hlin\archetype\CLI
$context->fs // => \hlin\archetype\Filesystem
$context->web // => \hlin\archetype\Web
$context->logger // => \hlin\archetype\Logger
// etc
$context->cli_is($cli)
$context->fs_is($fs)
$context->web_is($web)
$context->logger_is($logger)
$context->addpath('example', '/example/path');
$context->path('example'); // => /example/path
hlin.archetype.Filesystem
The archetype is used for accessing filesystem related functions. Almost
all functions have the same signature as the equivalent PHP function with the
same name. You wish to use the Filesystem version to allow for testing which
is useful for overwriting and testing. You'll generally almost always use it
via a Context
object.
$fs->file_exists($filename);
$fs->unlink($filename);
$fs->chmod($filename, $mode);
$fs->copy($source, $dest);
$fs->file_get_contents($filename);
$fs->file_put_contents($filename, $data);
$fs->file_append_contents($filename, $data);
$fs->filemtime($filename);
$fs->is_readable($filename);
$fs->mkdir($filename, $mode, $recursive);
$fs->realpath($path);
$fs->scandir($directory);
// etc
You may use FilesystemMock
when writing tests.
See Contextual archetype for help on using contexts.
hlin.archetype.Configs
The archetype is used for reading configuration values; primarily using the
read
method. You'll generally almost always use it via a
Context
object. See
Contextual archetype for help
on using contexts.
$confs->read('your/configuration');
The configuration file should be placed in in the confs
directory of any of your module. The configuration loader uses the autoloader's
file mapping to merge all configuration files so module you place the
configuration will only affect the order by which other identical configuration
files in another module overwrite it's values.
If you've setup up a console you can run the
module-stack
command to view how modules stack up in your system.
You can always adjust the stacking order via the matches-before
directive, see Defining a Module for
examples.
hlin.archetype.CLI
The archetype is used for cli related functions. You'll generally almost
always use it via a Context
object. See
Contextual archetype for help
on using contexts.
$cli->passthru($command);
$cli->system($command);
$cli->exec($command);
$cli->printf($message);
$input = $cli->fgets();
There are a few specialized methods to assist in creating console applications. All fairly explainatory.
$answer = $cli->ask('Continue?', ['Y', 'n']);
// user answers: Y<EnterKey> => 'Y'
// user answers: n<EnterKey> => 'n'
// question will be re-asked until a valid answer is given
// given: php script.php do:something --flag1 abc flag2
$cli->args(); // => [ 'php script.php', 'do:something', ... ]
$cli->syscall(); // => php script.php
$cli->command(); // => do:something
$cli->flags(); // => [ '--flag1', 'abc', 'flag2' ]
hlin.archetype.Web
The archetype is used for web related functions. You'll generally almost
always use it via a Context
object. See
Contextual archetype for help
on using contexts.
$web->requestMethod();
$web->requestUri();
$web->requestBody();
$web->postData();
$web->header($header, $replace, $http_response_code);
$web->http_response_code();
$web->redirect($url, $type);
$web->send($contents, $status, $headers);
hlin.archetype.Logger
The archetype is used for logging related functions. You'll generally almost
always use it via a Context
object. See
Contextual archetype for help
on using contexts.
$logger->logexception($exception);
$logger->log($message);
$logger->var_dump($var);
hlin.archetype.Autoloader
The Autoloader archetype is purely for wrapping autoloaders for use in a standardized way. Some classes may require an autoloader, so it's better if they require a standardized interface over requiring an explicit class.
The archetype also helps in imposing requirements and conventions.
hlin.archetype.Filemap
Like the Autoloader archetye this is also specifically meant for wrapping an existing concept for generic consumtion, rather then defining any particular type.
You are unlikely to have to implement one yourself.
In general a file map is a representation of the multi-module file system employed in freia. It facilitates reading/merging of configuration, loading files, etc.
hlin.attribute.Contextual
The attribute allows you to easily create classes that can accept a context. Here is an example:
<?php namespace your\module;
use \hlin\attribute\Contextual;
class Example implements Contextual {
use \hlin\ContextualTrait;
/**
* @return static
*/
static function instance(\hlin\archetype\Context $context) {
$i = new static;
$i->context_is($context);
return $i;
}
} # class
hlin.attribute.Configurable
The attribute helps creating classes that accept configurable objects. The attribute allows you to create classes that can read configurations but can't do much else (presumably intentionally as a constraint on the implementation)
The interface/trait provides the confs_are
method along
with $confs
easily accessible attribute in the class.
<?php namespace yourmodule\system;
use \hlin\attribute\Configurable;
class Example implements Configurable {
use \hlin\ConfigurableTriat;
// ...
} # class
hlin.tools.FileLogger
A file logger is the most basic type of logger (aside from NoopLogger which does nothing). To instantiate one you only need a Filesystem context and logs path.
$logspath = realpath("$syspath/logs");
$fs = \fenrir\Filesystem::instance();
$logger = \hlin\FileLogger::instance($fs, $logspath);
Generally this will happen in your main context initialization block.
You can optionally pass a strings array for "beclouding." The strings will be replaced inside the logs both for bravety and clarity, as well as security concerns. In general the strings are merely frequently used paths.
$logger = \hlin\FileLogger::instance (
$fs, $logspath, ['syspath' => $syspath] );
hlin.tools.NoopLogger
As the name implies a logger that does nothing. Generally invoked by normal
loggers that "do something" when they fail catestrophically due to permission
errors or otherwise, by returning a NoopLogger
they ensure the
system itself doesn't have to enter an error state due to logging failing.
$do_nothing_logger = \hlin\NoopLogger::instance();
hlin.tools.Request
A request object is a wrapper on the elements of a request; which is to
say it isolates the handler that accepts a Request
object from
the current real state of, in many cases, what would be the Web
context.
$req->requestUri(); // => '/threads/12'
$req->param('thread') // => '12'
$req->requestMethod(); // => post
$req->input(['json'], 'array'); // => [ 'id' => '1234' ]
// etc
hlin.tools.Response
A response object is used to delay the response until the very last seconds,
essentially it avoids working directly with, what is many cases, the
Web
context. By doing so if any error occurs there is no nasty
errors such as "headers already sent" or similar and the system can gracefully
recover; or at least has a chance to.
$res->responseCode(200);
$res->header('Content-Type: text/plain');
$res->redirect('/login', 303);
The class also helps with encapsulating logic. Instead of injecting your logic in every control handler you can simply specify the logic on the response wrapper and have all handlers just pass in input and/or configuration.
$res->logic(function ($input, $conf) { ... });
// ...
$controller = function ($req, $res) {
$res->conf('example');
return [ 'message' => 'hello, world' ];
};
// ...
$raw_output = $controller($req, $res);
$res->parse($raw_output);
hlin.tools.PHP
The PHP
class contains php language utilities.
PHP::unn
Convert from PHP Namespace Name to Universal Namespace Name.
\hlin\PHP::unn('\hlin\tools\Text'); // => hlin.tools.Text
In general you'll want to use the PHP::unn
when displaying
signatures as it's a lot clearer to read.
PHP::pnn
Convert from Universal Namespace Name to PHP Namespace Name.
\hlin\PHP::pnn('hlin.tools.Text'); // => \hlin\tools\Text
It's recomended to use universal names in configuration files. Simply use this function to convert to PHP namespaces that can be invoked.
hlin.tools.Arr
The Arr
class contains array utilities. The name is shortened
due to limitations in the language ("Array" is reserved). The class contains
only static helpers.
Arr::join
Arr::join($glue, array $list, callable $manipulator)
is used
as an advanced version of PHP standard implode
that allows for
manipulation of the value. The Arr::join
also has a convenient
"if false
the value from the list is ignored" so it works in both
filtering and joinging at the same time.
$list = [ 'x' => 10, 'y' => 20, 'z' => 30 ];
$res = \hlin\Arr::join(";\n", $list, function ($key, $val) {
if ($val == 20) return false;
return "$key -> $val";
});
x -> 10;
z -> 30;
The manipulator signature is always ($key, $value)
(parameter
names are not important), if you wish to simply "implode on value" please
consider just using implode
with array_map
, this will
save the Arr
class needing to be autoloaded; or the
hlin.tools
module as a dependency in general.
$res = implode($glue, array_map(function ($val) {
// your code
}, $list);
If you simply need to implode booleans (as rare as that is) you can just use:
$res = implode($glue, array_map(function ($key, $val) {
// your code
}, array_keys($list), $list));
hlin.tools.Text
The Text
class contains text manipulation functions. While
specialized it doesn't not contian functions for advanced concepts such as
internationalization, only basic helpers (all static).
Text::reindent
The function has the following signature:
Text::reindent
(
$source,
$indent = '',
$tabs_to_spaces = 4,
$ignore_zero_indent = true
)
$source
is your input. $indent
is what you want
as an indent after after normalization. $tabs_to_spaces
converts
tabs to spaces before processing. $ignore_zero_indent
specifies if
lines with no indentation should be ignored when detecting indentation for
normalization.
$res = \hlin\Text::reindent
(
'
aaa
bbb
ccc
ddd
eee
',
' -> '
);
-> aaa
-> bbb
-> ccc
-> ddd
-> eee
function is primarly meant to be used internally
hlin.tools.Time
The Time
class contains date and time manipulation
functions.
Time::timezoneOffset
This helper simply returns the numeric offset of the timezone given; DST will affect the result.
\hlin\Time::timezoneOffset('America/New_York'); // => -4:00
\hlin\Time::timezoneOffset('Etc/Universal'); // => +0:00
\hlin\Time::timezoneOffset('Europe/London'); // => +1:00
function is primarly meant to be used internally
fenrir
ModuleThe fenrir
module specilizes in coupled code, either literally
(explicit language such as SQL, explicit dependencies such as PHP or vendor
libraries) or in the abstract sense (explicit technologies, explicit use cases,
non-portable techniques).
To use a database you'll need to configure one. The database classes provided by this module take the configuration when being instanced so you can provide the configuration from anywhere.
If you wish to take advantage of tools such as pdx
you will
need to provide the configuration in a known location. Normally, which is to
say if you haven't overwritten the modules in question (namely fenrir.tools.PdxCommand::dbs
), that location will be
the configuration file freia/databases
.
Here is how a simple database configuration looks like:
<?php return [
'example.mysql' => [
'dsn' => 'mysql:host=localhost;dbname=freia-example;charset=utf8',
'username' => 'root',
'password' => 'root',
'options' => [
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'
],
'attributes' => [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC
],
'timezone' => date_default_timezone_get(),
'pdx:lock' => false,
]
]; # conf
The configuration is very similar to PDO because that's used under the hood for most abstractions.
Paradox is a migration systems that is designed to sync your database to your source. It's customizable, so you can tailor it to any kind of migrations with a little bit of work. By default it comes ready configured for sql/mysql and compatible databases migrations.
The system is module-aware which is to say you can have per-module migrations, with inter module dependencies and it will handle them properly, or notify you if you created an impossible scenario.
You can access the system from anywhere but in general you'll want to access
it from the command line, all commands are fairly self explainatory, and by
default will do a dry-run until you add the !
flag.
# View databases known to system
$ server/console pdx list
# Show history of the demo.mysql database
$ server/console pdx log demo.mysql
# Dry-run for setup of demo.mysql for the first time
$ server/console pdx init demo.mysql
# Setup demo.mysql for the first time
$ server/console pdx init demo.mysql !
# Dry-run for deletion of demo.mysql
$ server/console pdx rm demo.mysql
# Delete demo.mysql as it's know by history
$ server/console pdx rm demo.mysql !
# Hard delete demo.mysql
$ server/console pdx rm demo.mysql --hard !
# Dry-run for syncing the demo.mysql database
$ server/console pdx sync demo.mysql
# Sync demo.mysql database
$ server/console pdx sync demo.mysql !
Migrations are done though configuration files, most steps allow for both arrays (ie. configuration) as well as callbacks for if you need to perform more special handling.
By default, and in particular if you're using the pdx
console
command, you place the configuration of your migration in
freia/paradox
(which you can create in any module), the first
key specifies the "migrations module" (choose anything; we recomend grouping
into one module as much as possible, easier to keep in sync), the second key
specifies the version of the migration, and the value specifies the
configuration that holds the migration logic.
<?php return [
'example-main' => [
'1.0.0' => 'timeline/example/install',
]
]; # conf
Migrations have a lot of steps, you can customize, in general most migrations only use one or two (since you're usually either only creating, only updating, or only modifying structure, not everything at once).
<?php return [
'description'
=> 'Install for basic example tables.',
'configure' => [
'tables' => [
'forums',
'threads',
'posts'
]
],
'create:tables' => [
'forums' =>
'
_id [primaryKey],
title [title],
PRIMARY KEY (_id)
',
'threads' =>
'
_id [primaryKey],
forum [foreignKey],
title [title],
PRIMARY KEY (_id)
',
'posts' =>
'
_id [primaryKey],
body [block],
PRIMARY KEY (_id)
',
],
'bindings' => [
'threads' => [
// table onDelete onUpdate idKey
// -------- --------- --------- -----
'forum' => [ 'forums', 'CASCADE', 'CASCADE', '_id' ],
// specifying the id is optional, will default to _id
]
]
]; # conf
Here's another migration:
<?php return [
'description'
=> 'Add thread column.',
'modify:tables' => [
'posts' =>
'
ADD COLUMN `thread` [foreignKey] AFTER `_id`
',
],
'bindings' => [
'posts' => [
'thread' => [ 'threads', 'CASCADE', 'CASCADE' ],
]
]
]; # conf
Need a migration that's dependent on some other modules? Specify the value for the version as an array, with the 2nd component in the array an array of modules and version they need to be at before this module can reach the version specified by the migration.
<?php return [
'example-main' => [
'1.0.0' => 'timeline/example/install',
'1.1.0' => [
'timeline/example/1.1.0', [
'example-access' => '1.0.0',
'some-other-module' => '2.1.4'
]
]
]
]; # conf
Basic HTTP
routing is performed though
HttpDispatcher
. The class works on the principle of matching rules,
if a rule at any point matches all subsequent rules won't match.
$http = \fenrir\HttpDispatcher::instance($context);
$routes = $context->confs->read('freia/routes');
if ($http->matches('/api/.*')) {
$http->response()->logic(function ($response, $conf) {
return json_encode($response);
});
$http->any($routes['hello'], function ($req, $res) {
return ['say' => 'Hello'];
});
// 404
if ($http->nomatch()) {
$context->web->send(json_encode(['error' => 'no such api']), 404);
}
}
else { // web app
$http->response()->logic(function ($response, $conf) {
return render("webapp:$conf", $response);
});
// ...
// 404
if ($http->nomatch()) {
$context->web->send(render('public:404'), 404);
}
}
fenrir.system.MysqlDatabase
Wrapper around PDO, alters defeaults and enhances system to be more fluent and easier to write by adding shorters methods, and a whole bunch of helpers (including mass assignment helpers).
$entry = $db->prepare
(
'
SELECT entry.*
FROM `[table]` entry
WHERE entry_id = :id
LIMIT 1
',
$this->constants()
)
->num(':id', $id)
->execute()
->fetch_entry();
fenrir.system.MysqlRepoTrait
Provides basic, easily enhanced, functionality for creating a Repository, ie. a data abstraction layer based on mysql or compatible SQL database.
The trait provides the following public methods:
find(array $logic = null) => array(\hlin\archetype\Model)
entry($entry_id) => \hlin\archetype\Model
store(\hlin\archetype\Model $model) => \hlin\archetype\Model
All methods expect models and return models. Where model, in the Repository pattern is meant to be just a wrapper around data with only basic business logic and validation, no data binding, no persistance layer knowledge.
Here is a very minimalistic Repo class.
<?php namespace example\linker;
/**
* ...
*/
class TodoRepo implements \hlin\archetype\Repo {
use \fenrir\MysqlRepoTrait;
/**
* @return array
*/
function constants() {
return [
'model' => 'example.Todo',
'table' => 'todos'
];
}
/**
* @return static
*/
static function instance(\fenrir\MysqlDatabase $db) {
$i = new static;
$i->db = $db;
return $i;
}
} # class
find
MethodFind works similar to the mongodb data access api and always returns an array of models. The models are specified by constraints on the Repo class.
Here are a few self explaintory examples:
# retrieve all entries
$all_entries = $repo->find();
# retrieve 10 entries
$entries = $repo->find([ '%limit' => 10 ]);
# retrieve 10 entries of status "active"
$entries = $repo->find([
'status' => 'active',
'%limit' => 10
]);
# retrieve 10 entries, starting after then 5th
$entries = $repo->find([
'%limit' => 10,
'%offset' => 5
]);
# all entries, but only _id and title
$entries = $repo->find([
'%fields' => [ '_id', 'title' ]
]);
# all entries, order by title
$entries = $repo->find([
'%order_by' => [ 'title' => 'asc' ]
]);
# all entries, some complex constraints
$entries = $repo->find([
'title' => [ 'like' => '%Cat' ],
'type1' => [ '<=' => 1, '>' => -6],
'type2' => 'active',
'type3' => null,
'type4' => [ '!=' => null ],
'type5' => [ 'between' => ['2014-01-01', '2015-01-01'] ],
'type6' => [ 'in' => [2, 4, 6, 8] ]
]);
entry
MethodRetrieves a entry or returns null
.
# retrieve an entry
$entryModel = $repo->entry($entry_id);
Use find to retrieve based on criteria.
store
MethodPersists an entry, returns a new entry corresponding to the persisted one, since the model provided in might not have full data (commonly missing id).
# persist entry in the repository
$persistedCat = $catRepo->store($catModel);
The class provides a bunch of protected methods that reflect intermediary
steps feel free to use them to hook in functionality such as joining tables.
We won't go over them here due to any use of these method requiring internal
knowledge, but if you need to add JOINs for example you would overwrite the
sqlselect
method, if you need to add more constraint options
you would overwrite parseconstraints
, etc
fenrir.tools.MakeCommand
Make command helps in creating classes. The command will try to figure out
what you want to make and fill in the gaps, so you dont have to. You can
explicitly teach it patterns though the freia/make/patterns
configuration.
# Get available domains
$ ./console make ?
# Create class FooCommand (that implements \hlin\archetype\Command) with
# namespace example; place it in the file /Command/Foo.php located in the
# class path for the module specific to namespace example
$ ./console make example.FooCommand
# Same as above only we ensure that the problem example.FooCommand is
# interpreted as a class
$ ./console make class:example.FooCommand
To create a new pattern add the following to a
freia/make/patterns
configuration file:
<?php return [
'Cat' => [
'aliases' => [
'catkind.SmartAnimal' => 'Animal'
],
'extends' => 'Animal',
'implements' => [ 'catkind.Overlord' ],
'use' => [ 'hlin.CommandTrait' ],
'placeholder' =>
"\t/**\n\t * @return int\n\t */\n".
"\tfunction main(array \$args = null) {\n".
"\t\t// TODO implement\n".
"\t}"
],
];
The key there specifies the matching pattern. In our case any class ending
with dog. So it will match when we try to create something.FunnyCat
or example.something.InternetCat
, etc.
In the example, aliases are converted to use statements. So in our case we
are using an alias for the extends part, just to make the prezentation nicer.
There is no real difference between the way it's done in the example and simply
using catkind.SmartAnimal
for the extends
block and
ignoring the aliases
block altogheter.
Traits are specified via the use
block.
To specify PHP native or global classes you have to use the
.TheClass
syntax, eg. .Exception
ran
ModuleThe ran
module specialises in theme and view helpers; the
module is seperated primarily to facilitate static analysis.
The HTML context is a very simple wrapper for automatically configuring more complex objects.
$html = \ran\HTML::instance($context->confs);
$form = $html->form('/example/action');
The ran module provides a object based form builder. You will need a HTML context to easily use them. The class is primarily oriented towards creating complex static pages, a lot of them. The form helpers allows you to simply declare the form and control all it's rendering, including hints, errors, and other goodies from configuration files. You can have multiple configurations in the same project and you can also customize fields on the spot.
<?= $f = $html->form('/example/action', 'your-configuration-name') ?>
<?= $f->select('Stuff', 'people')->options_array(['A', 'B', 'C']) ?>
<?= $f->text('Given Name', 'given_name') ?>
<button type="submit" <?= $f->mark() >>Send</button>
The first assignment above forces the form object to render. The form will
automatically close, if you need to support IE8 you'll need to add a polyfill
for the form
attributes.
ran.tools.Temp
Temp
is used for easily creating placeholder content.
<?php namespace ran; ?>
<div class="person">
<div class="person-Avatar">
<img src="<?= Temp::img(100, 100) ?>"/>
</div>
<div class="person-Meta">
<p>name: <?= Temp::name() ?></p>
<p>signup: <?= date('Y-m-d', Temp::time()) ?></p>
<p>tel: <?= Temp::telephone() ?></p>
</div>
</div>
While you can use it like the above example the real magic of Temp is that when you call (almost all) the methods you get an instance that resolves to a random version of what you requested. Here is a simple example of the concept:
$name = \ran\Temp::given_name();
echo $name;
echo $name;
echo $name;
echo $name;
The above may render to...
Ana
Bob
Eve
Henry
Of course what it renders to is random, your results may vary.
Most methods of Temp
are self explainatory: name
,
given_name
, family_name
, word
,
words
, paragraph
, city
,
address
, ssn
, email
,
telephone
, title
, fullurl
,
counter
.
You can use copies
to generate arrays of random things. This
method takes an array of data and creates an array of copies from it. In
additin to that you can pass in a 3rd parameter that specifies which of the
values in your original array are counters and it will increment or randomize
based on what you specify.
// create random number of copies of entry with random number for viewcount
// and random number for comments, and incrementing value for id
$entries = Temp::copies($entry, rand(-20, 10), [
'viewcount' => [0, 1000],
'comments' => [0, 1000],
'id' => 1,
]);
When the count is lower then 0 it's interpreted as 0. The reason for negative values is so you can specify how often you want to get an empty state.
ran.tools.HH
HH
stands for HTMLHeader
and maintains utilities
for programatically managing html header levels. The purpose is to get
perfect header levels.
HH::next(); # => 'h1'
HH::next(); # => 'h2'
HH::raise('h2'); # => 'h3'
HH::raise('h6'); # => 'h6'
You should always store your header units in variables and use them later; using the functions directly isn't practical.
$h1 = HH::next();
$h2 = HH::next();
// ...
<?= "<$h2>My Title</$h2>" ?>
Note that $h1
isn't necesarily 'h1'
and
$h2
isn't necesarily 'h2'
. If the piece of code in
the example was in a partial view the headers could resolve to h3
and h4
.
It is recomended you pass the last header to partials; by convention
$h
in the following:
HH::base($h)
$h1 = HH::next();
$h2 = HH::next();
The following is equivalent to the above.
$h1 = HH::raise($h);
$h2 = HH::raise($h1);
If you ever need to reset to h1
, simply call
HH::base(null)
, or you can just hard code it by doing
$h1 = 'h1'
and move from there.
hlin.Console
(up to hlin.tools 1.3.2
),
hlin.MakeCommand
and fenrir.ConfCommand
(up to
fenrir.tools 1.1.6
) used to optionally require
rootpath
to be defined for formatting purposes; this was
confusing since syspath
is whats considered
the root of the system (ie. lowest project directory; whatever that may
be). This has been normalized to syspath
. While
hlin.FileLogger
is not affected since it does not care what
paths you pass in to obscure, if you were using rootpath, you may wish to
change it to syspath to avoid confusionhlin.ModelTrait
(up to hlin.archetype 1.2.1
)
implied the property attrs
for accessing data, this was changed
to data
from hlin.archetype 1.3.0
onwardarchetype (context related)
archetype (freia)
attribute
tools (logging)
tools (routing)
tools (utils)
system
tools