There is no license information available for the latest version (0.0.0) of this package.

0.0.0 2019-12-21 04:58 UTC

README

avatar

SeanMorris/Ids v00.0.0

/ eye dee ess / • The PHP + Docker Framework

seanmorris/ids Apache-2.0 Licence Badge CircleCI Codecov Size badge Docker Pulls - Idilic Docker Pulls - Server

php 8.0 tests php 8.1 tests php 8.1 tests

The Ids library provides general domain-primitives for developing web based applications. Routing, requests, modeling configuration, logging, sessions, and database access are all abstracted behind simple, expressive interfaces to efficient and powerful code.

To prevent unexpected behavior, the system is set to die on all errors down to E_NOTICE, excluding E_DEPRECATED errors generated from the vendor directory.

The project is made to run in docker but doesn't require it. It can be included in any composer project, and used in part or in whole easily.

The philosophy of the Ids project is headlined by security, speed and easy of use, in that order.

View the docs at docs.ids.seanmorr.is

Dependencies

expand
  • Composer
  • Docker
  • Docker Compose
  • GNU Make
  • Git
  • Linux or Compatible OS
  • PHP
  • SimpleTest
  • Minikube (required for kubernetes targets only)

Installing

expand Include Ids in your project with:
$ composer require seanmorris/ids

Install Ids globally for access to the idilic cli tool:

$ composer global require seanmorris/ids:dev-master

Add composer's global vendor/bin to your PATH by adding this to your ~/.bashrc.

export PATH="$HOME/.composer/vendor/bin:$PATH"

Creating a New Ids Project

expand

Create a new project with composer, enter the directory and start php, apache & mysql:

$ composer create-project seanmorris/ids-project -s dev --remove-vcs
$ cd ids-project
$ make @dev build start-bg

Thats it!

The companion package that provides a template for new projects can be found here:

https://github.com/seanmorris/ids-project

Dev Tools

expand

The dev build target provides facilities for connecting to xdebug and graylog.

XDebug

XDebug is built into the dev images by default. You can configure it by setting XDEBUG_CONFIG_ environment variables in .env.dev. By default it will attempt to connect to port 9000 on ${DHOST_IP}, which is the machine runing the project.

Apt Cache

Rebuild your images offline

Build the project with +aptcache with an internet connection to populate your cache.

$ make @dev+aptcache build

... to be continued

GrayLog

\SeanMorris\Ids\Logger\Gelf provides a simple interface to graylog. Just add it to the IDS_LOGGERS_ environment variable to enable it. So long as there is a graylog server available , it will send all logs that meet the verbosity threshold.

The default graylog config for the dev target looks like:

IDS_LOGGERS_=\SeanMorris\Ids\Logger\Gelf
IDS_GRAYLOG_HOST=graylog
IDS_GRAYLOG_PORT=12201

This package comes with a default GELF TCP input in its graylog config backup. You can run make graylog-restore after starting graylog for the first time to create the input.

The graylog config can be backed up and restored with the following commands:

$ make graylog-backup     # alias glbak
$ make graylog-restore    # alias glres

Graylog can be started and stopped with the following commands:

$ make graylog-start      # alias gls
$ make graylog-start-fg   # alias glsf
$ make graylog-start-bg   # alias glsb

$ make graylog-stop       # alias gld

$ make graylog-restart    # alias glr
$ make graylog-restart-fg # alias glrf
$ make graylog-restart-bg # alias glrb

Some users

sysctl -w vm.max_map_count=262144

Idilic CLI

expand

Ids comes with the idilic command when installed globally with composer. When executed, it will ascend through the current path looking for another project including Ids. When it finds it, it will attach to that project and utilize its facilities.

If it encounters a local idilic pass-thru in the project directory, it will hand control to the docker environment where execution will continue.

Run idilic help to see actions exposed by any installed packages.

Multistaging / Environment Targets

expand

base, prod, dev, and test

By default Ids provides 4 build targets: base, prod, dev, and test. Each exposes different ports, so they may run without conflict in parallel.

  • base exposes no ports and builds without require-dev.

  • prod exposes port 80 (configurable by IDS_EXPOSE_HTTP) and port 443* ( configurable by IDS_EXPOSE_HTTPS ) and builds without require-dev.

  • test exposes port 2021 (configurable by IDS_EXPOSE_HTTP) and port 3031 ( IDS_EXPOSE_SQL ) and builds with require-dev.

  • dev exposes port 2020 ( configurable by IDS_EXPOSE_HTTP ) and port 3030 ( IDS_EXPOSE_SQL ) and builds with require-dev.

*Not yet implemented.

Switching Targets

The system will use the TARGET environment variable to decide which build target to use.

If youre on the BASH shell simply run one of the following commands to set the target for the context of a single command:

  • make @base start
  • make @dev build
  • make @test test
  • make @prod push-images

If you don't feek like typing @target for every command, you can ask the system to remember a target with:

  • make stay@base
  • make stay@dev
  • make stay@test
  • make stay@prod

Extending Environments

Additional targets may be specified by creating a new infra/compose/[TARGET].yml in your project. Build this file as you'd build any other docker-compose file.

Then create a new .env.[TARGET] file in config/. Add your defaults here.

If you need some target specific build steps. then add a FROM base as TARGET section to any docker files in infra/docker that are relevant to that build target. You can expand on any existing build target, you're not limited to extending base.

Build

expand

The project may be built with make. Debian users can get this tool by running apt-get install build-essential. The build process also requires docker and docker-compose. minikube is required only for kubernetes testing.

The default build target is base. run make stay@dev or make stay@test to switch to the development/testing target. You can use @TARGET at the beginning of any make command to use the target for just that command.

$ make @dev build # build the project

$ make @dev start # start the services

Docker & docker-compose are available here:

Start / Stop / Restart

expand

Make sure to set a target with make stay@target before issuing any commands. Alternatively you can run commands in the form make @target command.

$ make start      # Start the project in the background,
                  # with no output

$ make start-fg   # Start the project in the foreground.

$ make start-bg   # Start the project in the background,
                  # stream output to foreground.

$ make stop       # Stop all services defined for the target.

$ make stop-all   # Stop all services spawned for the target
                  # even ones no longer in target compose file.

$ make restart    # Stop, then restart the project in the background,
                  # with no output.

$ make restart-fg # Stop, then restart the project in the foreground.

$ make restart-bg # Stop, then restart the project in the background,
                  # stream output to foreground.

Images & Tags

expand
$ make list-images # List all images for the current project, target & branch.

$ make list-tags   # List all tags for the current project, target & branch.

$ make push-images # push all images for the current project, target & branch.

$ make pull-images # push all images for the current project, target & branch.

Autotagging / Autopublishing / Git Hooks

expand

Register git hooks with make hooks.

Image tags are generated automatically on build based on the date, current git tag (falls back to commit hash).

The master branch will generate:

  • repository/project:gitTag-target
  • repository/project:date-target
  • repository/project:latest-target

Branches other than master will generate:

  • repository/project:gitTag-target-branch
  • repository/project:date-target-branch
  • repository/project:latest-target-branch

Images will be built on git commit and pushed on git push if the current branch appears in the project root .publishing file in the form: BRANCH:TARGET. For example this file would push images for prod & test when git push is run for the master branch:

master:test
master:prod

Available Images

expand

Docker images for seanmorris/ids.idilic & seanmorris/ids.server for targets base, dev, and test are available for use & extension on DockerHub:

Pull from the cli with:

$ docker pull seanmorris/ids.idilic:latest
$ docker pull seanmorris/ids.server:latest

or extend in a dockerfile with one of the following:

(it is not recommended to use latest in FROM)

FROM seanmorris/ids.idilic:TAGNAME
FROM seanmorris/ids.server:TAGNAME

Service Switches

expand Additional services can be included when running Ids. Simply add +toolname to your @target when issing a make command, and the necessary composer files will be included in the comtext of your command. For example, to start the system with graylog in dev mode:
make @dev+graylog start

Or inotify

make @dev+inotify start

Or both

make @dev+graylog+inotify start

Templating

expand

This ain't your granpappy's makefile.

GNU Make has a distinct and powerful syntax. It borrows, or in some instances outright uses the underlying bash engine to allow a user to define complex build steps and track the relationships between them to ensure everything is up to date.

Make allows for recursive variable expansion, conditionals, loops, and even calls out to the shell. This syntax is no longer limted to the Makefile.

Simply put the extension *.idstmp.* before your existing file extension. Ids will look for these files in all subdirectories of the prohect and rebuild them on startup. The resulting file will have the extension *.___gen.* where the *.idstmp.* is in the source file.

The resulting files should be excluded from version control, as they may contain artifacts from one target that should not exist in another.

For example: infra/docker/aptcache.idstmp.dockerfile starts off with the following line:

FROM ${BASELINUX}

Varibles are not normally allowed in the FROM section of dockerfile, preprocessed by make before it is used. So long as the file extenstion begins with .idstmp., we can count on a .___gen. file being produced. This allows us to keep all the images and containers synced to one base image.

The follwing lines from the end of the file show how one can use the shell to track who generated the file and when:

# generated @ $(shell date)
# by ${shell whoami} @ ${shell hostname}

See Functions for Transforming Text for more information.

Configuration / Environment Variables / Secrets

expand

Loading Settings

Settings may be provided in environment variables, .env files, or yml files.

Environment Variable & Target Files

The following files may be created/modified to configure the system. When the project is built, restarted, etc, they will be checked for modification, and re-built to the root of the projct if need be. The files generated to the root of the project should not be committed to version control.

  • config/.env - Should not be committed to version control. Contains configuration that applies to the system regardles of the system's target.
  • config/.env.default - Should be committed to version control. Contains non-secret configurations file and blank/dfault values for variables to be set in config/.env.
  • config/.env_TARGET - Should not be committed to version control. Contains configuration based the system's target.
  • config/.env_TARGET.default - Should be committed to version control. Contains non-secret configurations file and blank/dfault values for variables to be set in config/.env_TARGET.

The values set will be read according to the following precedence (higher takes precedence over lower):

  • .env_TARGET
  • .env_TARGET.default
  • .env
  • .env.default

Environment variables to be used in configuration should have the the prefix IDS_. An environment variable with the name IDS_SOME_VAR and IDS_SOME_OTHERVAR would be accessible within the system with:

IDS_SOME_VAR=value
IDS_SOME_OTHERVAR=other value
<?php
use \SeanMorris\Ids\Settings;

$someVar = Settings::read('some', 'var');
$someOtherVar = Settings::read('some', 'otherVar');

Configuration Objects

They'd also both be accessible as an object, which can be iterated:

<?php

$some = Settings::read('some');

$some->var;
$some->otherVar;

foreach($some as $configKey => $value)
{

}

Configuration Arrays

Arrays can be created by adding an underscore to the end of the environment variable name. The value will be split on whitespace.

IDS_ARRAY_=first second third
<?php
$array = Settings::read('ARRAY');

Whitespace will be preserved if the value is quotes:

IDS_ARRAY_=first "second element" third

Quotes can be escaped by doubling:

IDS_ARRAY_=first "second element ""with quotes inside""." third

Hostname & Port based configuration

Hostname specific environment variables are prefixed with an extra underscore: IDS__. Dots and other non-word charaters in the hostname are placed by a single underscore, except for the hyhpen which is replaced by 3 underscores. Another double underscore finishes the hostname, and the variable name comes next.

The above environment variables could be overridden for example.com with: IDS__EXAMPLE_COM__SOME_VAR and IDS__EXAMPLE_COM__SOME_OTHERVAR. They would be accessed in the same way as above:

IDS__EXAMPLE_COM__SOME_VAR=overridden value
IDS_SOME_OTHERVAR=other overridden value
<?php
use \SeanMorris\Ids\Settings;

$someVar = Settings::read('some', 'var');
$someOtherVar = Settings::read('some', 'otherVar');

$some = Settings::read('some');

$some->var;
$some->otherVar;

Example.com could override the variables by port number if they wanted to change some behavior based on whether the user was on SSL. Adding another double underscore between the hostname and the variable name allows us to do that: IDS__EXAMPLE_COM__80__SOME_VAR and IDS__EXAMPLE_COM__80_SOME_OTHERVAR for HTTP and IDS__EXAMPLE_COM__443__SOME_VAR and IDS__EXAMPLE_COM__443_SOME_OTHERVAR for HTTPS.

Again, nothing changes in way they are accessed. The code reads them in the same way:

IDS__EXAMPLE_COM__80__SOME_VAR=normal value
IDS__EXAMPLE_COM__443__SOME_VAR=value for SSL

IDS__EXAMPLE_COM__443__SOME_VAR=some other value
IDS__EXAMPLE_COM__443_SOME_OTHERVAR=some other value for SSL
<?php
use \SeanMorris\Ids\Settings;

$someVar = Settings::read('some', 'var');

// ...and so on

Non-Secret Config

Default values for environment variables that are non-secrets, (and thus may be pushed to version control) may be set in config/.env. Target specific variables may be set in config/.env.[TARGET]. DO NOT PUT SECRETS SUCH AS PASSWORDS HERE. These files will be checked into version control.

These files will be used to generate the final .env files in the root of the project when the project is built or started.

Disposable Secrets

If you need a quick random value to use in the build process (ie when creating a MYSQL password), make sure to add the key to the relevant .env file, but leave the value blank. Add the key name to the .entropy file in the root of your project in the form: CONFIG_KEY:ENTROPY_KEY. CONFIG_KEY is the name of your configuration value, and ENTROPY_KEY is an arbitrary string that allows you to re-use the same random value between different configuration keys.

When the project starts, the .env files will be generated to the root of the project if they do not exist already, or if the existing ones are empty. Any keys present in the .entropy file will be set to a random 32 character string. This is how the schema name, username, and password are generated for the mysql server for the make test command when if the current TARGET is test.

run TARGET=test idilic info to see an example of this.

Normal Secrets

So long as the .env files exist, and are not empty, the system will not attempt to regenerate them, so any secrets could in theory be added here, in place. This works fine for development, but for production it is recomended that secrets be stored in environtment variables.

Values from environment variables and .env files may accessd via PHP's getenv(). Ensure you've set the relevent values in the environment section of the target's docker-compose file if you chose not to add the value to a .env file.

Json/Yaml Configuration

Settings

Json/Yaml files may be provided in the config/ directory, under a directory named hostname/ or hostname;port/, where hostname and port are the domain and port you expect requests to come in on. The file should then be named settings.json or settings.yml. A directory named _;80 or ;80 will match only for requests on port 80 regardless of the hostname. A directory named _;, _, or ';' may be provided as a fallback if no others match.

NOTE If any yml config file is loaded, the system stops there and does not look for others. Host or port specific yml files will not be merged with more general wild card ones.

Defaults

Default, non-secret values may be provided in a file named settings.defaults.yml or settings.defaults.json. Defaults are the same as settings except they're stored in version control.

Defaults are loaded by the same rules as standard settings files. Once a singe defaults file is found no more will be loaded.

Settings and Defaults files do not need to be loaded at the same level of generality. settings.yml may be found in _;80 and settings.defaults.yml may be found in hostname or vice versa.

Settings always take precedence over defaults.

Environment variables from the shell or .env files take precedence over yml files.

to be continued...

Schema Diffing & Patching

expand

Ensure you've installed the idilic cli tool from the start of the document. Use the following commands to manage your database schema.

idilic storeSchema [PACKAGE] - Store the current schema.

idilic applySchema [PACKAGE] - Apply the stored schema.

idilic applySchemas - Apply the stored schema for all installed packages.

The schema will be stored in data/global/schema.json.

Routing

expand
IDS_ENTRYPOINT=SeanMorris\Ids\Test\Route
<?php
namespace SeanMorris\Ids\Test\Route;
class RootRoute implements \SeanMorris\Ids\Routable
{
	public
		$routes = [
			'foo' => 'SeanMorris\Ids\Test\Route\FooRoute'
		]
		, $alias = [
			'bar' => 'foo'
		];

	public function index($router)
	{
		return 'index';
	}

	public function otherPage($router)
	{
		return 'not index';
	}
}

Modeling / ORM

expand

Defining models

  • Properties
  • Selectors
  • Column wrappers
  • Behavior
<?php
namespace SeanMorris\Ids\Test\Model;
class Foozle extends \SeanMorris\Ids\Model
{
	use Common;

	protected
		$id
		, $class
		, $publicId
		, $value
	;

	protected static
		$table = 'Foozle'

		, $createColumns = [
			'publicId' => 'UNHEX(REPLACE(UUID(), "-", ""))'
		]

		, $byId = [['id' => '?']]

		, $readColumns = [
			'publicId' => 'HEX(%s)'
		]

		, $updateColumns = [
			'publicId' => 'UNHEX(%s)'
		]
	;
}

Loading Models

Model load methods are dynamically generated with names in the form of:

VERB [Flat] [Submodel|Record] [By SELECTOR]([...$args])

Verbs

  • get - Gets an array of models.
  • getOne - Gets a single model.
  • load - Returns a callback that gets an array of models.
  • loadOne - Alias of getOne.
  • generate - Returns a callback that produces a Generator of models.
  • count - Returns a count of models.
  • report - Return an (optionally) aggregated result set as a 2d array.

Modifiers

  • Flat - Don't join child models, even if specified in selector (parent class tables are still joined)
  • Submodel - Include any subclass of a given model in result set (May not include all tables!);
  • Record - Load records but skip initialization logic (before/afterRead methods).

Selectors

Logging

expand Logs can be written from anywhere in the system by calling a function coresponging to the desied level of verbosity. There are 6 levels of verbosity available.
<?php
use \SeanMorris\Ids\Log;

Log::trace(...$messages); # Log a message along with a stacktrace.
Log::query(...$messages); # Log query-level information
Log::debug(...$messages); # Log debug information
Log::info(...$messages);  # Log general information
Log::warn(...$messages);  # Issue a warning
Log::error(...$messages); # Issue an error

Ids will write logs to a file or handle specified by the IDS_LOG config:

IDS_LOG=php://stderr
# can also be a file
IDS_LOG=/tmp/ids.log

The current level of verbosity can be configured with the IDS_LOGLEVEL variable.

NOTE: If LOGLEVEL is set to trace, every single log entry will be accompanied by a stack trace. This can fill a disk quickly. Use with caution!

IDS_LOGLEVEL=trace
IDS_LOGLEVEL=query
IDS_LOGLEVEL=info
IDS_LOGLEVEL=debug
IDS_LOGLEVEL=warn
IDS_LOGLEVEL=error
IDS_LOGLEVEL=off

ANSI Colors

Logging comes with ANSI color turned on by default. Set the following config to disable it if thats causing problems for whatever you're using to read the logs:

IDS_LOGCOLOR=0

Censoring Logs

Set the IDS_LOGCENSOR_ array in your config to define an array of values that should not appear in log files. If these values are found as function arguments, array keys, or object properties, they will be logged as * censored * rather than their actual value.

The default censor filter is:

IDS_LOGCENSOR_=router password passwd

You can test censor filters. Issuing the following command will output your database settings, with no censorship applied:

$ idilic config databases

Adding -vv to the idilic command will force it to print its logs to STDERR (without verbosity checks).

$ idilic -vv config databases

We can drop the original output of the command as follows to declutter the terminal:

$ idilic -vv config databases 1 > /dev/null

We can also extract the original output with:

$ idilic -vv config databases 2 > /dev/null

Redirecting / Forcing CLI Logs

Logging can be forced on the command line with -v and -vv.

  • -v will print any logs within the current verbosity thesholds to STDERR.
  • -vv will print any logs REGARDLESS of verbosity thesholds to STDERR.
$ idilic -v  info
$ idilic -vv info

Logs of individual commands can be redirected to files or handles with 2>.

$ idilic -v  info 2> /tmp/summary$(date +%Y%m%d).log
$ idilic -vv info 2> /tmp/details$(date +%Y%m%d).log

If you're more interested in the logs than the output, you can discard the output with:

$ idilic -v  command 1> /dev/null
$ idilic -vv command 1> /dev/null

Graylog

Logs may be sent to Graylog by providing a custom log handler in the config

IDS_LOGGERS_=\SeanMorris\Ids\Logger\Gelf

Custom Loggers

Custom loggers may created by implemeting the \SeanMorris\Ids\Logger interface.

<?php
namespace SeanMorris\Ids\Logger;
class AdditionalLogger implements \SeanMorris\Ids\Logger
{
	public static function start($logBlob)
	{/* ... */}

	public static function log($logBlob)
	{/* ... */}
}

Add them to the IDS_LOGGERS_ array to activate them:

IDS_LOGGERS_=\SeanMorris\Ids\Logger\AdditionalLogger

Debugging

expand
XDEBUG_CONFIG_REMOTE_HOST=${DHOST_IP}
XDEBUG_CONFIG_REMOTE_PORT=9000

XDEBUG_CONFIG_PROFILER_ENABLE=0
XDEBUG_CONFIG_REMOTE_ENABLE=1

Linking Packages

expand

Some features require foreknowledge of what packages, files, and classes are present. Scanning for this list every time one of those features is used would slow things to a crawl. Build this index with the following commands:

$ make composer-dumpautoload
$ idilic link

Testing

expand
$ make @test build test
$ idilic runTests
<?php
namespace SeanMorris\ExamplePackage\Test;
class ExampleTest extends \UnitTestCase
{
	public function testSomething()
	{
		// ...
	}
}

File Access

expand
<?php
use \SeanMorris\Ids\Disk\File;

$file = File::open($filename);

//Check if file exists
if($file->check())
{
	// ...
}

//Get the path to the file
$path = $file->name();

//Copy the file to another location.
$copiedFile = $file->copy($newLocation);

// Read byte-by-byte
while(!$file->eof())
{
	$byte = $file->read(1);
}

// Read entire file:
$content = $file->slurp();

// Append
$file->write($content);

// Overwrite
$file->write($content, FALSE);

Migrations*

expand to be completed

Asset Management

expand to be completed

HTTP API*

expand to be completed

IPC / AMPQ*

expand to be completed

Implmenting Idilic CLI Commands

expand

Idilic commands can be run from the CLI in the form idilic command or idilic Vendor/Package command. Note that on the CLI package names can be separated by a forward slash (/). This is done to prevent the user from being forced to remember to escape backslashes (\).

Run idilic help to get a list of available idilic commands.

New commands can be implemented by adding a route class named RootRoute under the namespace Vendor\Package\Idilic\Route like so:

<?php
namespace SeanMorris\ExamplePackage\Idilic\Route;
class RootRoute implements \SeanMorris\Ids\Routable
{
	/** Help text goes here. */
	public function commandName($router)
	{}
}

Dependency Injection

expand

Some classes provide injection constructors. They'll take one or more classes as an argument and in return will give you a new class to work with.

For example, the SeanMorris\Ids\Collection class provides a method that will return a new subclass that will work only with the given type:

In this example, DatetimeCollection does not exist until afte the ::of() method completes, but PHP will still allow us to access ::CLASS on it.

We also alias the DatetimeCollection class with use BEFORE it exists. This prevents it from inheriting the Author\Package namespace in this scenario, although it is not necessary.

<?php
namespace Author\Package;

use \DatetimeCollection;
use \SeanMorris\Ids\Collection;

// Create DatetimeCollection based on the existing Collection class
Collection::of(Datetime::CLASS, DatetimeCollection::CLASS);

// Create an instance of the new class:
$datetimeCollection = new DatetimeCollection();

Creating Injectables

Creating a new injectable class from scratch is easy. You can either inherit from an existing class that uses the SeanMorris\Ids\Injectable trait, or create an entirely new class from scratch with the following construct:

NOTE: If you override the constructor, you must either call parent::__construct() or $this->initInjections(); in your subclass contructor to ensure your injections are ready when your object are instantiated.

<?php

$injectableClass = (new class { use Injectable; })::inject([]);

You could now use new $injectableClass to instantiate a class with very little functionality. The problem here is that you can't inherit from an anonymous class in PHP. To solve this problem we can pass a second parameter to ::inject() to name the class:

<?php

use \SeanMorris\Ids\Injectable;

(new class { use Injectable; })::inject([], AwesomeInjectable::CLASS);

class AwesomeClass extends AwesomeInjectable
{
	public function someMethod()
	{
		// here there be behaviors...
	}
}

Class Promotion

The last example showed something called class promotion, this simply allows us to take an anonymous class and "promote" it to a named class to that other parts of the system can refer to it by name.

There are two ways to promote a class:

With the Injectable trait:

<?php
use \SeanMorris\Ids\Injectable;

$anonymousClass = new class
{
	use Injectable;

	public function doSomething()
	{
		echo "I'm doing something.";
	}
};

// Pr
$anonymousClass::inject([], NamedClass::CLASS);

Or with the Loader class:

<?php

$anonymousClass = new class
{
	public function doSomething()
	{
		echo "I'm doing something.";
	}
};

Loader::define([ NamedClass::CLASS => $anonymousClass ]);

Defining Injections

In ::inject()'s' first parameter, we can define any default injections we'd like our class to have, to facilitate situations where we'd want it to have access to a default set of behaviors we can override.

<?php
use \SeanMorris\Ids\Injectable;

(new class { use Injectable; })::inject([

	InjectedDate::CLASS => Datetime::CLASS

], AwesomeInjectable::CLASS);

You can now access the injections as a static property of the newly created class:

<?php

class DateFormatter extends AwesomeInjectable
{
	protected static $InjectedDate;

	public function dateToTimestamp($date)
	{
		$datetime = new static::$InjectedDate($date);

		return $datetime->getTimestamp();
	}
}

Since the injections are represented by static properties, injectables are accessible just fine in the static scope:

<?php

class DateFormatter extends AwesomeInjectable
{
	protected static $InjectedDate;

	public static function dateToTimestamp($date)
	{
		$datetime = new static::$InjectedDate($date);

		return $datetime->getTimestamp();
	}
}

You can continue to create further subclasses that may or may not override the default injections:

<?php
// Inherit injected classes normally:

class CoolDateFormatter extends DateFormatter
{
	// ...
}

// Or create new subclasses by injecting the class and passing a new name:
// (AwesomeDateFormatter is being created based on DateFormatter here)

DateFormatter::inject([

	InjectedDate::CLASS => \Awesome\Project\AwesomeDatetime::CLASS

], AwesomeDateFormatter::CLASS);

class EvenCoolerDateFormatter extends AwesomeDateFormatter
{
	// ...
}

Subclassing existing/default injections

If the existing injection is a static property, you can simply acesss it and call ::inject() on it to create a subclass of the existing injection:

<?php

use \SeanMorris\Ids\Collection;
use \SeanMorris\Ids\WrappedMethod;

$RankIterator = $collectionClass::$RankIterator::inject([
	'map' => WrappedMethod::wrap($callback)
]);

$mappedCollection = Collection::inject([

	'RankIterator' => $RankIterator

]);

Or you could promote the class and extend it directly:

If the existing injection is a static property, you can simply acesss it and call ::inject() on it to create an injected subclass:

<?php
use \SeanMorris\Ids\Collection;
use \SeanMorris\Ids\WrappedMethod;

$collectionClass::$RankIterator::inject([

	'map' => WrappedMethod::wrap($callback)

], \InjectedRankIterator::CLASS);

class SubInjectedRankIterator extends InjectedRankIterator
{
	//...
}

$mappedCollection = Collection::inject([

	'RankIterator' => SubInjectedRankIterator::CLASS

]);

Creating injectables out of existing classes

Any class that can be extended can become injectable:

<?php
use \SeanMorris\Ids\Injectable;

class RegularOldClass
{
	// ...
}

(new class() extends RegularOldClass { use Injectable; })::inject(
	[], InjectableRegularOldClass::CLASS
);

$object = new InjectableRegularOldClass();

Factories, Singletons & Injectable Methods

Behaviors may be dynamically provided as injections by wrapping closure with a simple class to let the system know how to treat it.

The methods are wrapped by classes so they may participate in the Loader system. See Global Injections for more information on that topic.

Injected Methods

The WrappeMethod class allows you to pass a method along that will not be called by the system, allowing it to be used in code.

RankIterator implements the following method in such a way that a map method can be injected:

<?php

class RankIterator extends AppendIterator
{
	use Injectable;

	protected static $map;

	// ...

	public function current()
	{
		$value = $this->getInnerIterator()->current();

		if(static::$map)
		{
			$mapper = static::$map;
			$value  = $mapper($value, $this->key());
		}

		return $value;
	}

	// ...
}

The new iterator class with mapping behavior can be created like so:

<?php

use \SeanMorris\Ids\WrappedMethod;
use \SeanMorris\Ids\Collection\RankIterator;

$MappedRankIterator = RankIterator::inject([

	'map' => WrappedMethod::wrap(function($input){
		$output = doSomething($input);

		return $output;
	})

]);

$iterator = new $MappedRankIterator;

Factory Methods

Factory methods may be provided in a similar manner.

<?php
use \SeanMorris\Ids\Inject\FactoryMethod;

$coolDateFormatter = AwesomeDateFormatter::inject([

	assembledObject::CLASS => FactoryMethod::wrap(function(){

		$object = new StdClass;

		$object->someProperty = 'important value';
		$object->someOtherVar = 'slightly less important value';

		return $object;
	})

]);

Singleton Methods

Singletons may be loaded in a similar manner. A method wrapped by SingletonMethod will be called only once and its return value used as the provided injection for all cases.

Singletons provided as static properties will be instatiated on definition, rather than on property access.

<?php
use \SeanMorris\Ids\Inject\SingletonMethod

class AwesomeLogger
{
	protected $fileHandle;

	writeLog($line)
	{
		fwrite($this->fileHandle, $line);
	}
}

$CoolerLogger = AwesomeLogger::inject([

	logFile::CLASS => SingletonMethod::wrap(function(){

		$fileHandle = fopen(LOG_FILE_LOCATION, 'a');

		fwrite("Log started!\n", $fileHandle);

		return $fileHandle;
	})
]);

$logger = new $CoolerLogger;

$logger->writeLine('This is a log line!');

Global injections & \___\... namespaces

Injections can be defined globally so that classes can just pick up on them and go. Using the \___\... namespace, we can set up places where injections can be defined globally. You can also use the \Author\Project\___\... namespace.

For example, if the AwesomeLogger class from above were defined like this:

<?php

$injections = [logFile::CLASS => \___\LogFileInjectable::CLASS];

(new class { use Injectable; })::inject($injections, AwesomeLogger::CLASS);

An injection can be defined globally in the ids.boot.php file in the source/ directory of the project. This can be overridden as the root project's boot file will be executed last.

<?php
use \SeanMorris\Ids\Loader;

// We can alias classes even if they don't exist yet:
// If we were in a namespace, this would prevent it
// from inheriting the FQNS.
use \___\LogFileInjectable;

Loader::define([ LogFileInjectable::CLASS => ActualLogFileClass::CLASS ]);

Overriding global injections

Global injections may only be overridden before they are used in code. This does not count access to ::CLASS or use statments that import classes from other namespaces.

<?php
// Dependency package's ids.boot.php:
use \SeanMorris\Ids\Loader;

Loader::define([ LogFileInjectable::CLASS => LogFileClass::CLASS ]);
<?php
// Root package's ids.boot.php:
use \SeanMorris\Ids\Loader;

Loader::define([ LogFileInjectable::CLASS => AwesomeLogFileClass::CLASS ]);

Fallback global injections

Sometimes you might want to allow injections to be overriden for the whole of the system or perhaps some of its parts. If you want this behavior as well as a fallback to a default the following pattern will handle that.

In this example, any part of the system may ask for "Red Paint", "Blue Paint", or "Just whatever Paint". They might not get the color they asked for, but they will get paint.

<?php
// Dependency package's ids.boot.php:
use \SeanMorris\Ids\Loader;

Loader::define([ \___\Paint::CLASS      => BasicPaint::CLASS ]);

Loader::define([ \___\Paint\Red::CLASS  => \___\Paint::CLASS ]);
Loader::define([ \___\Paint\Blue::CLASS => \___\Paint::CLASS ]);

This would allow us to override all instances where "Paint" is injected.

<?php
// Root package's ids.boot.php:
use \SeanMorris\Ids\Loader;

Loader::define([ \___\Paint::CLASS => AwesomePaint::CLASS ]);

This would allow us to override only instances where "Red Paint" is injected.

<?php
// Root package's ids.boot.php:
use \SeanMorris\Ids\Loader;

Loader::define([ \___\Paint\Red::CLASS => AwesomeRedPaint::CLASS ]);

Sessions

expand to be completed

Email

expand
<?php
use \SeanMorris\Ids\Mail;

$mail = new \SeanMorris\Ids\Mail;
$mail->body(<<<EOM
	Message body here.
EOM);

$mail->from(\SeanMorris\Ids\Settings::read('noreply'));

$mail->subject('Hello from Ids!');

$mail->to($recipientEmail);

$mail->send(TRUE);

Themes & Frontends

expand to be completed

Make Commands

expand

Run these from the project root to build and control the project infrastructure.

  • make build make b - Build the project
  • make env make e - Print the project's environment config.
  • make test make t- Run tests.
  • make test make t- Remove the generated configs,* even if they have been altered.*
  • make start make s- Start the project services.
  • make start-fg make sf- Start the project services, hold control of the terminal and stream output.
  • make start-bg make sb- Start the project services, hold control of the terminal and stream output.
  • make restart-fg make rf - Restart the project services, hold control of the terminal and stream output.
  • make restart-bg make rb- Restart the project services, hold control of the terminal and stream output.
  • make stop make d- Stop the project services.
  • make stop-all make da- Stop the project services, including any that no longer appear in the compose file.
  • make kill make k- Immediately kill the project services.
  • make nuke make nk* - Immediately kill all containers on the host. Not yet implemented.
  • make current-tag ct- Output the project tag for the current target & branch.
  • make list-tags make lt- List image tags for the current target & branch.
  • make list-images make li- List images for the current target & branch.
  • make push-images make psi- List images for the current target & branch.
  • make pull-images make pli- List images for the current target & branch.
  • make hooks - Initialize git hooks.
  • make composer-install make ci- Install composer packages.
  • make composer-update make co- Update composer packages.
  • make composer-dump-autoload make cda- Regenerate and dump composer autoload files..
  • make npm install PKG="[PACKAGE]" make ni- Run npm install inside the project.
  • make bash make sh- Get a bash prompt to an idilic container.
  • make run CMD="SERVICE [COMMAND]" make r- Run a command in a service container.

SeanMorris/Ids

Copyright 2011-2022 Sean Morris

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

--------------------------------------------------------------------------------
Language                      files          blank        comment           code
--------------------------------------------------------------------------------
SVG                              30              0           8957          65159
PHP                             111           3481            511          14781
JSON                             12              0              0          13051
Markdown                          3            983             16           2008
YAML                             27            107             11           1109
make                              2            145              9            612
Dockerfile                       11             64              5            308
Properties                        2              0              0             46
Bourne Shell                      4              9              0             40
Bourne Again Shell                2             17             17             23
XML                               2              0              0             22
INI                               4              0              0             17
HTML                              1              0              0             13
CSS                               1              0              0              1
JavaScript                        1              0              0              1
--------------------------------------------------------------------------------
SUM:                            213           4806           9526          97191
--------------------------------------------------------------------------------

built by sean @ Mon Feb 20 09:13:03 AM EST 2023