Freia - the PHP library

Jump to Table of Contents

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.

Installing

Existing Codebase

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.

Fresh Start

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.

Minimal version

$ 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.

Raw Autoloader

All out coding hermit mode,

{
	"extra": {
		"freia": {
			"load": [ "vendor" ]
		}
	},

	"require": {
		"freia/autoloader": "1.*"
	}
}

Don't care for filtering though modules?

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.

A more complete composer.json for completeness

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.

Basic framework for getting started

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

License

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.

Foreword

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).

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 Module

The 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-modules.

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).

Examples Briefing

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

Autoloading

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

Class Name Conventions

Classes written in freia modules follow the following conventions:

  • given multiple words in a CamelCase classname the last word, and last word only, is converted to a directory and the classname with out the last word in it is used as the file name
  • in namespace resolution, the namespace is always considered a segment even if a namespace with the exact name exists and is matched against all namespaces in order until a namespace that contains that segment is found
  • if the main segment of the namespace of the symbol that is being loaded is not known to be a main segment in the system the namespace is ignored and symbol not loaded
  • whenever a namespace ends with 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 namespace
  • we recognize php is case insensitive but calling classes from freia modules using the wrong case will result in undefined behavior (will work if it was previously loaded, or on window, fail otherwise due to file mapping)

Going 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.

Examples of the first rule
// 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
Examples of resolution

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

Initialization

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

Putting it all togheter

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:

src/main.php
<?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;
	}
}
public/index.php
<?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;
		}
	}
console

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

Defining a Module

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 Module

The hlin module specialises in portable and completely testable code; unlike the fenrir module which specialises on classes that require deep coupling.

Interfaces

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"

  • DO NOT use relative namespaces (interface namespaces are always absolute!), usage of relative namespaces in the context of freia modules is considered "undefined behavior"
  • DO NOT have namespaces that only hold signatures, you should have your signatures next to your implementations, if you think of it appropriate to have them in a seperate namespace treat them as attributes or archetypes and implement them as such.

And of course a few optional usage recomendations:

  • predeclare all namespaces
  • order namespaces and traits as follows: attributes, archetypes, signatures

usage example

<?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

declaration example

<?php namespace yourmodule\archetype;

interface Example
<?php namespace yourmodule\attribute;

interface Example
<?php namespace yourmodule\system;

interface ExampleSignature

Using Contexts

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.

Exceptions & Errors

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.

Advanced error handling

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.

implementation pattern I

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();
implementation pattern II

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
implementation pattern III

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

Authorizer & Protocols

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:

  • extremely fast — so you can churn out as many checks as you like with out negatively influencing the system
  • extremely flexible — need to give access to someone only if he's the cousin of one of the employees who are under the project manager that submitted a commit at exactly 6am on a sunday, when there was a blue moon outside? no problem, we got you covered
  • easy to read — in the interest of keeping your system transparent your internal logic should only contain the question of if someone is able, not the logic of how that test is performed; we help you do that easily
  • no nonsense — you either were explicity authorizer or the system assumes you don't have access; unless you were explicitly blacklisted in which case tough luck

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.

minimalist example

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

creating protocols

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.

whitelist
<?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
blacklist
<?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
aliaslist
<?php namespace hlin; return [

	'member' => [ '+members' ],
	'admin' => [ '+members' ],

]; # conf

Using the Console

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.

Creating Commands

To create a command you simply need to do two things:

  • write the class that implements hlin.archetype.Command
  • write the configuration for the 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.

verbose & complete command configuration
<?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
minimalistic command configuration aka. lazy
<?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

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

Honeypot Command

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 Module

The 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).

Setup Database

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 Migrations

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.

freia/paradox

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

timeline/example/install

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

interdependence

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

Routing

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.

Minimalistic example

$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 Method

Find 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 Method

Retrieves a entry or returns null.

# retrieve an entry
$entryModel = $repo->entry($entry_id);

Use find to retrieve based on criteria.

store Method

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

Overwriting

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 Module

The ran module specialises in theme and view helpers; the module is seperated primarily to facilitate static analysis.

HTML Context

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

Forms

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.

minimalist example

<?= $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.

Major Changes

  • 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 confusion
  • hlin.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 onward
Table of Contents