seanmorris / ids
Installs: 3 146
Dependents: 5
Suggesters: 0
Security: 0
Stars: 3
Watchers: 2
Forks: 0
Open Issues: 1
Requires (Dev)
- simpletest/simpletest: dev-master
This package is auto-updated.
Last update: 2024-12-21 01:34:29 UTC
README
SeanMorris/Ids v00.0.0
/ eye dee ess / • The PHP + Docker Framework
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:
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 byIDS_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:
- idilic https://hub.docker.com/repository/docker/seanmorris/ids.idilic
- server https://hub.docker.com/repository/docker/seanmorris/ids.server
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 inconfig/.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 inconfig/.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 toSTDERR
.-vv
will print any logs REGARDLESS of verbosity thesholds toSTDERR
.
$ 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 completedAsset Management
expand
to be completedHTTP API*
expand
to be completedIPC / AMPQ*
expand
to be completedImplmenting 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 completedexpand
<?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 completedMake Commands
expand
Run these from the project root to build and control the project infrastructure.
make build
make b
- Build the projectmake 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
- Runnpm install
inside the project.make bash
make sh
- Get a bash prompt to anidilic
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