A bunch of new updates from icehrm pro

This commit is contained in:
Thilina Hasantha
2019-02-03 13:55:39 +01:00
parent a75325fb52
commit 96b0ad8496
598 changed files with 74156 additions and 29979 deletions

View File

@@ -3,16 +3,16 @@
@version V5.11 5 May 2010 (c) 2000-2010 John Lim (jlim#natsoft.com). All rights reserved.
Latest version is available at http://adodb.sourceforge.net
Released under both BSD license and Lesser GPL library license.
Whenever there is any discrepancy between the two licenses,
Released under both BSD license and Lesser GPL library license.
Whenever there is any discrepancy between the two licenses,
the BSD license will take precedence.
Active Record implementation. Superset of Zend Framework's.
Version 0.92
See http://www-128.ibm.com/developerworks/java/library/j-cb03076/?ca=dgr-lnxw01ActiveRecord
See http://www-128.ibm.com/developerworks/java/library/j-cb03076/?ca=dgr-lnxw01ActiveRecord
for info on Ruby on Rails Active Record implementation
*/
@@ -44,7 +44,7 @@ class ADODB_Active_Table {
// $db = database connection
// $index = name of index - can be associative, for an example see
// http://phplens.com/lens/lensforum/msgs.php?id=17790
// http://phplens.com/lens/lensforum/msgs.php?id=17790
// returns index into $_ADODB_ACTIVE_DBS
function ADODB_SetDatabaseAdapter(&$db, $index=false)
{
@@ -58,23 +58,23 @@ function ADODB_SetDatabaseAdapter(&$db, $index=false)
break;
}
} else {
if ($d->db->_connectionID === $db->_connectionID && $db->database == $d->db->database) {
if ($d->db->_connectionID === $db->_connectionID && $db->database == $d->db->database) {
$obj = $d;
break;
}
}
}
if ($index == false) $index = sizeof($_ADODB_ACTIVE_DBS);
if(!isset($obj)) {
$obj = new ADODB_Active_DB();
$obj->db = $db;
$obj->tables = array();
$obj->tables = array();
}
$_ADODB_ACTIVE_DBS[$index] = $obj;
return $index;
}
@@ -82,8 +82,8 @@ function ADODB_SetDatabaseAdapter(&$db, $index=false)
class ADODB_Active_Record {
static $_changeNames = true; // dynamically pluralize table names
static $_quoteNames = false;
static $_foreignSuffix = '_id'; //
static $_foreignSuffix = '_id'; //
var $_dbat; // associative index pointing to ADODB_Active_DB eg. $ADODB_Active_DBS[_dbat]
var $_table; // tablename, if set in class definition then use it as table name
var $_tableat; // associative index pointing to ADODB_Active_Table, eg $ADODB_Active_DBS[_dbat]->tables[$this->_tableat]
@@ -103,7 +103,7 @@ class ADODB_Active_Record {
}
// should be static
static function SetDatabaseAdapter(&$db, $index=false)
static function SetDatabaseAdapter(&$db, $index=false)
{
//error_log("Coming into ".self::_pluralize(get_called_class())."'s SetDatabaseAdapter where ".get_class());
if(!$index || !isset($index)) {
@@ -111,19 +111,19 @@ class ADODB_Active_Record {
}
return ADODB_SetDatabaseAdapter($db, $index);
}
public function __set($name, $value)
{
$name = str_replace(' ', '_', $name);
$this->$name = $value;
}
// php5 constructor
function __construct($table = false, $pkeyarr=false, $db=false)
{
global $ADODB_ASSOC_CASE,$_ADODB_ACTIVE_DBS;
if ($db == false && is_object($pkeyarr)) {
$db = $pkeyarr;
$pkeyarr = false;
@@ -143,9 +143,9 @@ class ADODB_Active_Record {
if(isset($_ADODB_ACTIVE_DBS[self::_pluralize(get_called_class())])) {
$this->_dbat = self::_pluralize(get_called_class());
} else {
$this->_dbat = key($_ADODB_ACTIVE_DBS);
$this->_dbat = key($_ADODB_ACTIVE_DBS);
}
}
$this->_table = $table;
@@ -153,13 +153,13 @@ class ADODB_Active_Record {
$this->UpdateActiveTable($pkeyarr);
}
function __wakeup()
{
$class = get_class($this);
new $class;
}
static function _pluralize($table)
{
if (!ADODB_Active_Record::$_changeNames) return $table;
@@ -170,26 +170,26 @@ class ADODB_Active_Record {
$lastc2 = substr($ut,$len-2);
switch ($lastc) {
case 'S':
return $table.'es';
return $table.'es';
case 'Y':
return substr($table,0,$len-1).'ies';
case 'X':
case 'X':
return $table.'es';
case 'H':
case 'H':
if ($lastc2 == 'CH' || $lastc2 == 'SH')
return $table.'es';
default:
return $table.'s';
}
}
// CFR Lamest singular inflector ever - @todo Make it real!
// Note: There is an assumption here...and it is that the argument's length >= 4
function _singularize($tables)
{
if (!ADODB_Active_Record::$_changeNames) return $table;
$ut = strtoupper($tables);
$len = strlen($tables);
if($ut[$len-1] != 'S')
@@ -221,14 +221,14 @@ class ADODB_Active_Record {
$table->_hasMany[$foreignRef] = $ar;
# $this->$foreignRef = $this->_hasMany[$foreignRef]; // WATCHME Removed assignment by ref. to please __get()
}
// use when you don't want ADOdb to auto-pluralize tablename
static function TableHasMany($table, $foreignRef, $foreignKey = false, $foreignClass = 'ADODB_Active_Record')
{
$ar = new ADODB_Active_Record($table);
$ar->hasMany($foreignRef, $foreignKey, $foreignClass);
}
// use when you don't want ADOdb to auto-pluralize tablename
static function TableKeyHasMany($table, $tablePKey, $foreignRef, $foreignKey = false, $foreignClass = 'ADODB_Active_Record')
{
@@ -236,8 +236,8 @@ class ADODB_Active_Record {
$ar = new ADODB_Active_Record($table,$tablePKey);
$ar->hasMany($foreignRef, $foreignKey, $foreignClass);
}
// use when you want ADOdb to auto-pluralize tablename for you. Note that the class must already be defined.
// e.g. class Person will generate relationship for table Persons
static function ClassHasMany($parentclass, $foreignRef, $foreignKey = false, $foreignClass = 'ADODB_Active_Record')
@@ -245,7 +245,7 @@ class ADODB_Active_Record {
$ar = new $parentclass();
$ar->hasMany($foreignRef, $foreignKey, $foreignClass);
}
function belongsTo($foreignRef,$foreignKey=false, $parentKey='', $parentClass = 'ADODB_Active_Record')
{
@@ -256,24 +256,24 @@ class ADODB_Active_Record {
$ar->parentKey = $parentKey;
$ar->UpdateActiveTable();
$ar->foreignKey = ($foreignKey) ? $foreignKey : $foreignRef.ADODB_Active_Record::$_foreignSuffix;
$table =& $this->TableInfo();
$table->_belongsTo[$foreignRef] = $ar;
# $this->$foreignRef = $this->_belongsTo[$foreignRef];
}
static function ClassBelongsTo($class, $foreignRef, $foreignKey=false, $parentKey='', $parentClass = 'ADODB_Active_Record')
{
$ar = new $class();
$ar->belongsTo($foreignRef, $foreignKey, $parentKey, $parentClass);
}
static function TableBelongsTo($table, $foreignRef, $foreignKey=false, $parentKey='', $parentClass = 'ADODB_Active_Record')
{
$ar = new ADOdb_Active_Record($table);
$ar->belongsTo($foreignRef, $foreignKey, $parentKey, $parentClass);
}
static function TableKeyBelongsTo($table, $tablePKey, $foreignRef, $foreignKey=false, $parentKey='', $parentClass = 'ADODB_Active_Record')
{
if (!is_array($tablePKey)) $tablePKey = array($tablePKey);
@@ -284,8 +284,8 @@ class ADODB_Active_Record {
/**
* __get Access properties - used for lazy loading
*
* @param mixed $name
*
* @param mixed $name
* @access protected
* @return mixed
*/
@@ -293,9 +293,9 @@ class ADODB_Active_Record {
{
return $this->LoadRelations($name, '', -1, -1);
}
/**
* @param string $name
* @param string $name
* @param string $whereOrderBy : eg. ' AND field1 = value ORDER BY field2'
* @param offset
* @param limit
@@ -307,12 +307,12 @@ class ADODB_Active_Record {
$table = $this->TableInfo();
if ($limit >= 0) $extras['limit'] = $limit;
if ($offset >= 0) $extras['offset'] = $offset;
if (strlen($whereOrderBy))
if (strlen($whereOrderBy))
if (!preg_match('/^[ \n\r]*AND/i',$whereOrderBy))
if (!preg_match('/^[ \n\r]*ORDER[ \n\r]/i',$whereOrderBy))
$whereOrderBy = 'AND '.$whereOrderBy;
if(!empty($table->_belongsTo[$name]))
{
$obj = $table->_belongsTo[$name];
@@ -323,7 +323,7 @@ class ADODB_Active_Record {
{
if ($obj->parentKey) $key = $obj->parentKey;
else $key = reset($table->keys);
$arrayOfOne = $obj->Find($key.'='.$this->$columnName.' '.$whereOrderBy,false,false,$extras);
if ($arrayOfOne) {
$this->$name = $arrayOfOne[0];
@@ -332,7 +332,7 @@ class ADODB_Active_Record {
}
}
if(!empty($table->_hasMany[$name]))
{
{
$obj = $table->_hasMany[$name];
$key = reset($table->keys);
$id = @$this->$key;
@@ -345,11 +345,11 @@ class ADODB_Active_Record {
$this->$name = $objs;
return $objs;
}
return array();
}
//////////////////////////////////
// update metadata
function UpdateActiveTable($pkeys=false,$forceUpdate=false)
{
@@ -365,7 +365,7 @@ class ADODB_Active_Record {
$acttab = $tables[$tableat];
foreach($acttab->flds as $name => $fld) {
if ($ADODB_ACTIVE_DEFVALS && isset($fld->default_value))
if ($ADODB_ACTIVE_DEFVALS && isset($fld->default_value))
$this->$name = $fld->default_value;
else
$this->$name = null;
@@ -379,19 +379,19 @@ class ADODB_Active_Record {
@flock($fp, LOCK_SH);
$acttab = unserialize(fread($fp,100000));
fclose($fp);
if ($acttab->_created + $ADODB_ACTIVE_CACHESECS - (abs(rand()) % 16) > time()) {
if ($acttab->_created + $ADODB_ACTIVE_CACHESECS - (abs(rand()) % 16) > time()) {
// abs(rand()) randomizes deletion, reducing contention to delete/refresh file
// ideally, you should cache at least 32 secs
foreach($acttab->flds as $name => $fld) {
if ($ADODB_ACTIVE_DEFVALS && isset($fld->default_value))
if ($ADODB_ACTIVE_DEFVALS && isset($fld->default_value))
$this->$name = $fld->default_value;
else
$this->$name = null;
}
$activedb->tables[$table] = $acttab;
//if ($db->debug) ADOConnection::outp("Reading cached active record file: $fname");
return;
} else if ($db->debug) {
@@ -400,18 +400,18 @@ class ADODB_Active_Record {
}
$activetab = new ADODB_Active_Table();
$activetab->name = $table;
$save = $ADODB_FETCH_MODE;
$ADODB_FETCH_MODE = ADODB_FETCH_ASSOC;
if ($db->fetchMode !== false) $savem = $db->SetFetchMode(false);
$cols = $db->MetaColumns($table);
if (isset($savem)) $db->SetFetchMode($savem);
$ADODB_FETCH_MODE = $save;
if (!$cols) {
$this->Error("Invalid table name: $table",'UpdateActiveTable');
$this->Error("Invalid table name: $table",'UpdateActiveTable');
return false;
}
$fld = reset($cols);
@@ -421,14 +421,14 @@ class ADODB_Active_Record {
foreach($cols as $name => $fld) {
if (!empty($fld->primary_key)) $pkeys[] = $name;
}
} else
} else
$pkeys = $this->GetPrimaryKeys($db, $table);
}
if (empty($pkeys)) {
$this->Error("No primary key found for table $table",'UpdateActiveTable');
return false;
}
$attr = array();
$keys = array();
$ADODB_ASSOC_CASE = 2;
@@ -446,18 +446,18 @@ class ADODB_Active_Record {
$keys[strtolower($name)] = strtolower($name);
}
break;
case 1:
case 1:
foreach($cols as $name => $fldobj) {
$name = strtoupper($name);
if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value))
$this->$name = $fldobj->default_value;
else
$this->$name = null;
$attr[$name] = $fldobj;
}
foreach($pkeys as $k => $name) {
$keys[strtoupper($name)] = strtoupper($name);
}
@@ -465,7 +465,7 @@ class ADODB_Active_Record {
default:
foreach($cols as $name => $fldobj) {
$name = ($fldobj->name);
if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value))
$this->$name = $fldobj->default_value;
else
@@ -477,7 +477,7 @@ class ADODB_Active_Record {
}
break;
}
$activetab->keys = $keys;
$activetab->flds = $attr;
@@ -489,58 +489,58 @@ class ADODB_Active_Record {
}
if (isset($activedb->tables[$table])) {
$oldtab = $activedb->tables[$table];
if ($oldtab) $activetab->_belongsTo = $oldtab->_belongsTo;
if ($oldtab) $activetab->_hasMany = $oldtab->_hasMany;
}
$activedb->tables[$table] = $activetab;
}
function GetPrimaryKeys(&$db, $table)
{
return $db->MetaPrimaryKeys($table);
}
// error handler for both PHP4+5.
// error handler for both PHP4+5.
function Error($err,$fn)
{
global $_ADODB_ACTIVE_DBS;
$fn = get_class($this).'::'.$fn;
$this->_lasterr = $fn.': '.$err;
if ($this->_dbat < 0) $db = false;
else {
$activedb = $_ADODB_ACTIVE_DBS[$this->_dbat];
$db = $activedb->db;
}
if (function_exists('adodb_throw')) {
if (function_exists('adodb_throw')) {
if (!$db) adodb_throw('ADOdb_Active_Record', $fn, -1, $err, 0, 0, false);
else adodb_throw($db->databaseType, $fn, -1, $err, 0, 0, $db);
} else
if (!$db || $db->debug) ADOConnection::outp($this->_lasterr);
}
// return last error message
function ErrorMsg()
{
if (!function_exists('adodb_throw')) {
if ($this->_dbat < 0) $db = false;
else $db = $this->DB();
// last error could be database error too
if ($db && $db->ErrorMsg()) return $db->ErrorMsg();
}
return $this->_lasterr;
}
function ErrorNo()
function ErrorNo()
{
if ($this->_dbat < 0) return -9999; // no database connection...
$db = $this->DB();
return (int) $db->ErrorNo();
}
@@ -549,7 +549,7 @@ class ADODB_Active_Record {
function DB()
{
global $_ADODB_ACTIVE_DBS;
if ($this->_dbat < 0) {
$false = false;
$this->Error("No database connection set: use ADOdb_Active_Record::SetDatabaseAdaptor(\$db)", "DB");
@@ -559,7 +559,7 @@ class ADODB_Active_Record {
$db = $activedb->db;
return $db;
}
// retrieve ADODB_Active_Table
function &TableInfo()
{
@@ -568,8 +568,8 @@ class ADODB_Active_Record {
$table = $activedb->tables[$this->_tableat];
return $table;
}
// I have an ON INSERT trigger on a table that sets other columns in the table.
// So, I find that for myTable, I want to reload an active record after saving it. -- Malcolm Cook
function Reload()
@@ -580,21 +580,21 @@ class ADODB_Active_Record {
return($this->Load($where));
}
// set a numeric array (using natural table field ordering) as object properties
function Set(&$row)
{
global $ACTIVE_RECORD_SAFETY;
$db = $this->DB();
if (!$row) {
$this->_saved = false;
$this->_saved = false;
return false;
}
$this->_saved = true;
$table = $this->TableInfo();
if ($ACTIVE_RECORD_SAFETY && sizeof($table->flds) != sizeof($row)) {
# <AP>
@@ -613,7 +613,7 @@ class ADODB_Active_Record {
}
else
$keys = array_keys($row);
# <AP>
reset($keys);
$this->_original = array();
@@ -627,7 +627,7 @@ class ADODB_Active_Record {
# </AP>
return true;
}
// get last inserted id for INSERT
function LastInsertID(&$db,$fieldname)
{
@@ -635,32 +635,32 @@ class ADODB_Active_Record {
$val = $db->Insert_ID($this->_table,$fieldname);
else
$val = false;
if (is_null($val) || $val === false) {
// this might not work reliably in multi-user environment
return $db->GetOne("select max(".$fieldname.") from ".$this->_table);
}
return $val;
}
// quote data in where clause
function doquote(&$db, $val,$t)
{
switch($t) {
case 'L':
if (strpos($db->databaseType,'postgres') !== false) return $db->qstr($val);
case 'D':
case 'D':
case 'T':
if (empty($val)) return 'null';
case 'B':
case 'B':
case 'N':
case 'C':
case 'X':
if (is_null($val)) return 'null';
if (strlen($val)>1 &&
(strncmp($val,"'",1) != 0 || substr($val,strlen($val)-1,1) != "'")) {
if (strlen($val)>1 &&
(strncmp($val,"'",1) != 0 || substr($val,strlen($val)-1,1) != "'")) {
return $db->qstr($val);
break;
}
@@ -669,13 +669,13 @@ class ADODB_Active_Record {
break;
}
}
// generate where clause for an UPDATE/SELECT
function GenWhere(&$db, &$table)
{
$keys = $table->keys;
$parr = array();
foreach($keys as $k) {
$f = $table->flds[$k];
if ($f) {
@@ -684,17 +684,17 @@ class ADODB_Active_Record {
}
return implode(' and ', $parr);
}
function _QName($n,$db=false)
{
if (!ADODB_Active_Record::$_quoteNames) return $n;
if (!$db) $db = $this->DB(); if (!$db) return false;
return $db->nameQuote.$n.$db->nameQuote;
}
//------------------------------------------------------------ Public functions below
function Load($where=null,$bindarr=false)
{
$this->_where = $where;
@@ -705,31 +705,31 @@ class ADODB_Active_Record {
}
return $this->LoadFromRawQuery($qry, $bindarr);
}
function LoadFromRawQuery($qry, $bindarr=false) {
global $ADODB_FETCH_MODE;
$db = $this->DB(); if (!$db) return false;
$save = $ADODB_FETCH_MODE;
$ADODB_FETCH_MODE = ADODB_FETCH_NUM;
if ($db->fetchMode !== false) $savem = $db->SetFetchMode(false);
$row = $db->GetRow($qry,$bindarr);
if (isset($savem)) $db->SetFetchMode($savem);
$ADODB_FETCH_MODE = $save;
return $this->Set($row);
}
# useful for multiple record inserts
# see http://phplens.com/lens/lensforum/msgs.php?id=17795
function Reset()
{
$this->_where=null;
$this->_saved = false;
$this->_lasterr = false;
$this->_saved = false;
$this->_lasterr = false;
$this->_original = false;
$vars=get_object_vars($this);
foreach($vars as $k=>$v){
@@ -740,24 +740,24 @@ class ADODB_Active_Record {
$this->foreignName=strtolower(get_class($this));
return true;
}
// false on error
function Save()
{
if ($this->_saved) $ok = $this->Update();
else $ok = $this->Insert();
return $ok;
}
// false on error
function Insert()
{
$db = $this->DB(); if (!$db) return false;
$cnt = 0;
$table = $this->TableInfo();
$valarr = array();
$names = array();
$valstr = array();
@@ -771,7 +771,7 @@ class ADODB_Active_Record {
$cnt += 1;
}
}
if (empty($names)){
foreach($table->flds as $name=>$fld) {
$valarr[] = null;
@@ -782,7 +782,7 @@ class ADODB_Active_Record {
}
$sql = 'INSERT INTO '.$this->_table."(".implode(',',$names).') VALUES ('.implode(',',$valstr).')';
$ok = $db->Execute($sql,$valarr);
if ($ok) {
$this->_saved = true;
$autoinc = false;
@@ -797,32 +797,32 @@ class ADODB_Active_Record {
$this->$k = $this->LastInsertID($db,$k);
}
}
$this->_original = $valarr;
return !empty($ok);
}
function Delete()
{
$db = $this->DB(); if (!$db) return false;
$table = $this->TableInfo();
$where = $this->GenWhere($db,$table);
$sql = 'DELETE FROM '.$this->_table.' WHERE '.$where;
$ok = $db->Execute($sql);
return $ok ? true : false;
}
protected function intify($ret) {
if(is_numeric($ret)) {
return intval($ret);
} else {
return $ret;
return $ret;
}
}
function Aggregate($function, $column, $whereGroupBy, $bindarr=false) {
if(!in_array($function, ADODB_Active_Record::$_supportedAggregateFunctions)) {
throw new InvalidArgumentException("Unknown Aggregate Function $function");
@@ -833,11 +833,11 @@ class ADODB_Active_Record {
$db = $this->DB(); if (!$db || empty($this->_table)) return false;
return $db->GetOne("select $function($column) from ".$this->_table." where ". $whereGroupBy, $bindarr);
}
function Count($whereGroupBy, $bindarr=false) {
return $this->intify( $this->Aggregate("count", "*", $whereGroupBy, $bindarr) );
}
function CountDistinct($column, $whereGroupBy, $bindarr=false) {
if(!in_array($column, $this->GetAttributeNames())) {
throw new InvalidArgumentException("Unknown Column for CountDistinct $column");
@@ -845,7 +845,7 @@ class ADODB_Active_Record {
$db = $this->DB(); if (!$db || empty($this->_table)) return false;
return $this->intify( $db->GetOne("select count(distinct($column)) from ".$this->_table." where ". $whereGroupBy, $bindarr) );
}
// returns an array of active record objects
function Find($whereOrderBy,$bindarr=false,$pkeysArr=false,$extra=array())
{
@@ -853,7 +853,7 @@ class ADODB_Active_Record {
$arr = $db->GetActiveRecordsClass(get_class($this),$this->_table, $whereOrderBy,$bindarr,$pkeysArr,$extra);
return $arr;
}
// returns an array of active record objects
function FindFromRawQuery($query,$bindarr=false,$pkeysArr=false,$extra=array())
{
@@ -861,17 +861,17 @@ class ADODB_Active_Record {
$arr = $db->GetActiveRecordsClass2(get_class($this),$this->_table, $query,$bindarr,$pkeysArr,$extra);
return $arr;
}
// returns 0 on error, 1 on update, 2 on insert
function Replace()
{
global $ADODB_ASSOC_CASE;
$db = $this->DB(); if (!$db) return false;
$table = $this->TableInfo();
$pkey = $table->keys;
foreach($table->flds as $name=>$fld) {
$val = $this->$name;
/*
@@ -887,24 +887,24 @@ class ADODB_Active_Record {
if (is_null($val) && !empty($fld->auto_increment)) {
continue;
}
if (is_array($val)) continue;
$t = $db->MetaType($fld->type);
$arr[$name] = $this->doquote($db,$val,$t);
$valarr[] = $val;
}
if (!is_array($pkey)) $pkey = array($pkey);
if ($ADODB_ASSOC_CASE == 0)
if ($ADODB_ASSOC_CASE == 0)
foreach($pkey as $k => $v)
$pkey[$k] = strtolower($v);
elseif ($ADODB_ASSOC_CASE == 1)
elseif ($ADODB_ASSOC_CASE == 1)
foreach($pkey as $k => $v)
$pkey[$k] = strtoupper($v);
$ok = $db->Replace($this->_table,$arr,$pkey);
if ($ok) {
$this->_saved = true; // 1= update 2=insert
@@ -921,9 +921,9 @@ class ADODB_Active_Record {
$this->$k = $this->LastInsertID($db,$k);
}
}
$this->_original = $valarr;
}
}
return $ok;
}
@@ -932,14 +932,14 @@ class ADODB_Active_Record {
{
$db = $this->DB(); if (!$db) return false;
$table = $this->TableInfo();
$where = $this->GenWhere($db, $table);
if (!$where) {
$this->error("Where missing for table $table", "Update");
return false;
}
$valarr = array();
$valarr = array();
$neworig = array();
$pairs = array();
$i = -1;
@@ -948,10 +948,10 @@ class ADODB_Active_Record {
$i += 1;
$val = $this->$name;
$neworig[] = $val;
if (isset($table->keys[$name]) || is_array($val))
if (isset($table->keys[$name]) || is_array($val))
continue;
if (is_null($val)) {
if (isset($fld->not_null) && $fld->not_null) {
if (isset($fld->default_value) && strlen($fld->default_value)) continue;
@@ -969,8 +969,8 @@ class ADODB_Active_Record {
$pairs[] = $this->_QName($name,$db).'='.$db->Param($cnt);
$cnt += 1;
}
if (!$cnt) return -1;
$sql = 'UPDATE '.$this->_table." SET ".implode(",",$pairs)." WHERE ".$where;
$ok = $db->Execute($sql,$valarr);
@@ -980,21 +980,21 @@ class ADODB_Active_Record {
}
return 0;
}
function GetAttributeNames()
{
$table = $this->TableInfo();
if (!$table) return false;
return array_keys($table->flds);
}
};
function adodb_GetActiveRecordsClass(&$db, $class, $table,$whereOrderBy,$bindarr, $primkeyArr,
$extra)
{
$qry = "select * from ".$table;
if (!empty($whereOrderBy)) {
$qry .= ' WHERE '.$whereOrderBy;
}
@@ -1006,7 +1006,7 @@ function adodb_GetActiveRecordsClass2(&$db, $class, $table, $qry, $bindarr, $pri
{
global $_ADODB_ACTIVE_DBS;
$save = $db->SetFetchMode(ADODB_FETCH_NUM);
if(isset($extra['limit']))
{
@@ -1027,13 +1027,13 @@ function adodb_GetActiveRecordsClass2(&$db, $class, $table, $qry, $bindarr, $pri
}
$db->SetFetchMode($save);
$false = false;
if ($rows === false) {
if ($rows === false) {
return $false;
}
if (!class_exists($class)) {
$db->outp_throw("Unknown class $class in GetActiveRecordsClass()",'GetActiveRecordsClass');
@@ -1047,7 +1047,7 @@ function adodb_GetActiveRecordsClass2(&$db, $class, $table, $qry, $bindarr, $pri
$arrRef = array();
$bTos = array(); // Will store belongTo's indices if any
foreach($rows as $row) {
$obj = new $class($table,$primkeyArr,$db);
if ($obj->ErrorNo()){
$db->_errorMsg = $obj->ErrorMsg();
@@ -1055,7 +1055,7 @@ function adodb_GetActiveRecordsClass2(&$db, $class, $table, $qry, $bindarr, $pri
}
$obj->Set($row);
$arr[] = $obj;
} // foreach($rows as $row)
} // foreach($rows as $row)
return $arr;
}

View File

@@ -1 +0,0 @@
../../src

View File

@@ -0,0 +1,157 @@
<?php
namespace Consolidation\Config;
use Dflydev\DotAccessData\Data;
class Config implements ConfigInterface
{
/**
* @var Data
*/
protected $config;
/**
* TODO: make this private in 2.0 to prevent being saved as an array
* Making private now breaks backward compatibility
*
* @var Data
*/
protected $defaults;
/**
* Create a new configuration object, and initialize it with
* the provided nested array containing configuration data.
*
* @param array $data - Config data to store
*/
public function __construct(array $data = null)
{
$this->config = new Data($data);
$this->setDefaults(new Data());
}
/**
* {@inheritdoc}
*/
public function has($key)
{
return ($this->config->has($key));
}
/**
* {@inheritdoc}
*/
public function get($key, $defaultFallback = null)
{
if ($this->has($key)) {
return $this->config->get($key);
}
return $this->getDefault($key, $defaultFallback);
}
/**
* {@inheritdoc}
*/
public function set($key, $value)
{
$this->config->set($key, $value);
return $this;
}
/**
* {@inheritdoc}
*/
public function import($data)
{
return $this->replace($data);
}
/**
* {@inheritdoc}
*/
public function replace($data)
{
$this->config = new Data($data);
return $this;
}
/**
* {@inheritdoc}
*/
public function combine($data)
{
if (!empty($data)) {
$this->config->import($data, true);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function export()
{
return $this->config->export();
}
/**
* {@inheritdoc}
*/
public function hasDefault($key)
{
return $this->getDefaults()->has($key);
}
/**
* {@inheritdoc}
*/
public function getDefault($key, $defaultFallback = null)
{
return $this->hasDefault($key) ? $this->getDefaults()->get($key) : $defaultFallback;
}
/**
* {@inheritdoc}
*/
public function setDefault($key, $value)
{
$this->getDefaults()->set($key, $value);
return $this;
}
/**
* Return the class $defaults property and ensure it's a Data object
* TODO: remove Data object validation in 2.0
*
* @return Data
*/
protected function getDefaults()
{
// Ensure $this->defaults is a Data object (not an array)
if (!$this->defaults instanceof Data) {
$this->setDefaults($this->defaults);
}
return $this->defaults;
}
/**
* Sets the $defaults class parameter
* TODO: remove support for array in 2.0 as this would currently break backward compatibility
*
* @param Data|array $defaults
*
* @throws \Exception
*/
protected function setDefaults($defaults)
{
if (is_array($defaults)) {
$this->defaults = new Data($defaults);
} elseif ($defaults instanceof Data) {
$this->defaults = $defaults;
} else {
throw new \Exception("Unknown type provided for \$defaults");
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Consolidation\Config;
interface ConfigInterface
{
/**
* Determine if a non-default config value exists.
*/
public function has($key);
/**
* Fetch a configuration value
*
* @param string $key Which config item to look up
* @param string|null $defaultFallback Fallback default value to use when
* configuration object has neither a value nor a default. Use is
* discouraged; use default context in ConfigOverlay, or provide defaults
* using a config processor.
*
* @return mixed
*/
public function get($key, $defaultFallback = null);
/**
* Set a config value
*
* @param string $key
* @param mixed $value
*
* @return $this
*/
public function set($key, $value);
/**
* Import configuration from the provided nexted array, replacing whatever
* was here previously. No processing is done on the provided data.
*
* @deprecated Use 'replace'. Dflydev\DotAccessData\Data::import() merges, which is confusing, since this method replaces.
*
* @param array $data
* @return Config
*/
public function import($data);
/**
* Load configuration from the provided nexted array, replacing whatever
* was here previously. No processing is done on the provided data.
*
* TODO: This will become a required method in version 2.0. Adding now
* would break clients that implement ConfigInterface.
*
* @param array $data
* @return Config
*/
// public function replace($data);
/**
* Import configuration from the provided nexted array, merging with whatever
* was here previously. No processing is done on the provided data.
* Any data provided to the combine() method will overwrite existing data
* with the same key.
*
* TODO: This will become a required method in version 2.0. Adding now
* would break clients that implement ConfigInterface.
*
* @param array $data
* @return Config
*/
// public function combine($data);
/**
* Export all configuration as a nested array.
*/
public function export();
/**
* Return the default value for a given configuration item.
*
* @param string $key
*
* @return mixed
*/
public function hasDefault($key);
/**
* Return the default value for a given configuration item.
*
* @param string $key
* @param mixed $defaultFallback
*
* @return mixed
*/
public function getDefault($key, $defaultFallback = null);
/**
* Set the default value for a configuration setting. This allows us to
* set defaults either before or after more specific configuration values
* are loaded. Keeping defaults separate from current settings also
* allows us to determine when a setting has been overridden.
*
* @param string $key
* @param string $value
*/
public function setDefault($key, $value);
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Consolidation\Config;
interface GlobalOptionDefaultValuesInterface
{
/**
* Return an associative array of option-key => default-value
*/
public function getGlobalOptionDefaultValues();
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Consolidation\Config\Inject;
use Consolidation\Config\ConfigInterface;
use Consolidation\Config\Util\ConfigFallback;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputOption;
class ConfigForCommand implements EventSubscriberInterface
{
protected $config;
protected $application;
public function __construct(ConfigInterface $config)
{
$this->config = $config;
}
public function setApplication(Application $application)
{
$this->application = $application;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [ConsoleEvents::COMMAND => 'injectConfiguration'];
}
/**
* Before a Console command runs, inject configuration settings
* for this command into the default value of the options of
* this command.
*
* @param \Symfony\Component\Console\Event\ConsoleCommandEvent $event
*/
public function injectConfiguration(ConsoleCommandEvent $event)
{
$command = $event->getCommand();
$this->injectConfigurationForGlobalOptions($event->getInput());
$this->injectConfigurationForCommand($command, $event->getInput());
$targetOfHelpCommand = $this->getHelpCommandTarget($command, $event->getInput());
if ($targetOfHelpCommand) {
$this->injectConfigurationForCommand($targetOfHelpCommand, $event->getInput());
}
}
protected function injectConfigurationForGlobalOptions($input)
{
if (!$this->application) {
return;
}
$configGroup = new ConfigFallback($this->config, 'options');
$definition = $this->application->getDefinition();
$options = $definition->getOptions();
return $this->injectConfigGroupIntoOptions($configGroup, $options, $input);
}
protected function injectConfigurationForCommand($command, $input)
{
$commandName = $command->getName();
$commandName = str_replace(':', '.', $commandName);
$configGroup = new ConfigFallback($this->config, $commandName, 'command.', '.options.');
$definition = $command->getDefinition();
$options = $definition->getOptions();
return $this->injectConfigGroupIntoOptions($configGroup, $options, $input);
}
protected function injectConfigGroupIntoOptions($configGroup, $options, $input)
{
foreach ($options as $option => $inputOption) {
$key = str_replace('.', '-', $option);
$value = $configGroup->get($key);
if ($value !== null) {
if (is_bool($value) && ($value == true)) {
$input->setOption($key, $value);
} elseif ($inputOption->acceptValue()) {
$inputOption->setDefault($value);
}
}
}
}
protected function getHelpCommandTarget($command, $input)
{
if (($command->getName() != 'help') || (!isset($this->application))) {
return false;
}
$this->fixInputForSymfony2($command, $input);
// Symfony Console helpfully swaps 'command_name' and 'command'
// depending on whether the user entered `help foo` or `--help foo`.
// One of these is always `help`, and the other is the command we
// are actually interested in.
$nameOfCommandToDescribe = $input->getArgument('command_name');
if ($nameOfCommandToDescribe == 'help') {
$nameOfCommandToDescribe = $input->getArgument('command');
}
return $this->application->find($nameOfCommandToDescribe);
}
protected function fixInputForSymfony2($command, $input)
{
// Symfony 3.x prepares $input for us; Symfony 2.x, on the other
// hand, passes it in prior to binding with the command definition,
// so we have to go to a little extra work. It may be inadvisable
// to do these steps for commands other than 'help'.
if (!$input->hasArgument('command_name')) {
$command->ignoreValidationErrors();
$command->mergeApplicationDefinition();
$input->bind($command->getDefinition());
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Consolidation\Config\Inject;
use Consolidation\Config\Util\ConfigMerge;
/**
* Given an object that contains configuration methods, inject any
* configuration found in the configuration file.
*
* The proper use for this method is to call setter methods of the
* provided object. Using configuration to call methods that do work
* is an abuse of this mechanism.
*/
class ConfigForSetters
{
protected $config;
public function __construct($config, $group, $prefix = '', $postfix = '')
{
if (!empty($group) && empty($postfix)) {
$postfix = '.';
}
$this->config = new ConfigMerge($config, $group, $prefix, $postfix);
}
public function apply($object, $configurationKey)
{
$settings = $this->config->get($configurationKey);
foreach ($settings as $setterMethod => $args) {
$fn = [$object, $setterMethod];
if (is_callable($fn)) {
$result = call_user_func_array($fn, (array)$args);
// We require that $fn must only be used with setter methods.
// Setter methods are required to always return $this so that
// they may be chained. We will therefore throw an exception
// for any setter that returns something else.
if ($result != $object) {
$methodDescription = get_class($object) . "::$setterMethod";
$propertyDescription = $this->config->describe($configurationKey);
throw new \Exception("$methodDescription did not return '\$this' when processing $propertyDescription.");
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Consolidation\Config\Loader;
/**
* Load configuration files.
*/
abstract class ConfigLoader implements ConfigLoaderInterface
{
protected $config = [];
protected $source = '';
public function getSourceName()
{
return $this->source;
}
protected function setSourceName($source)
{
$this->source = $source;
return $this;
}
public function export()
{
return $this->config;
}
public function keys()
{
return array_keys($this->config);
}
abstract public function load($path);
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Consolidation\Config\Loader;
/**
* Load configuration files, and fill in any property values that
* need to be expanded.
*/
interface ConfigLoaderInterface
{
/**
* Convert loaded configuration into a simple php nested array.
*
* @return array
*/
public function export();
/**
* Return the top-level keys in the exported data.
*
* @return array
*/
public function keys();
/**
* Return a symbolic name for this configuration loader instance.
*/
public function getSourceName();
}

View File

@@ -0,0 +1,167 @@
<?php
namespace Consolidation\Config\Loader;
use Grasmash\Expander\Expander;
use Consolidation\Config\Util\ArrayUtil;
/**
* The config processor combines multiple configuration
* files together, and processes them as necessary.
*/
class ConfigProcessor
{
protected $processedConfig = [];
protected $unprocessedConfig = [];
protected $expander;
public function __construct($expander = null)
{
$this->expander = $expander ?: new Expander();
}
/**
* Extend the configuration to be processed with the
* configuration provided by the specified loader.
*
* @param ConfigLoaderInterface $loader
*/
public function extend(ConfigLoaderInterface $loader)
{
return $this->addFromSource($loader->export(), $loader->getSourceName());
}
/**
* Extend the configuration to be processed with
* the provided nested array.
*
* @param array $data
*/
public function add($data)
{
$this->unprocessedConfig[] = $data;
return $this;
}
/**
* Extend the configuration to be processed with
* the provided nested array. Also record the name
* of the data source, if applicable.
*
* @param array $data
* @param string $source
*/
protected function addFromSource($data, $source = '')
{
if (empty($source)) {
return $this->add($data);
}
$this->unprocessedConfig[$source] = $data;
return $this;
}
/**
* Process all of the configuration that has been collected,
* and return a nested array.
*
* @return array
*/
public function export($referenceArray = [])
{
if (!empty($this->unprocessedConfig)) {
$this->processedConfig = $this->process(
$this->processedConfig,
$this->fetchUnprocessed(),
$referenceArray
);
}
return $this->processedConfig;
}
/**
* To aid in debugging: return the source of each configuration item.
* n.b. Must call this function *before* export and save the result
* if persistence is desired.
*/
public function sources()
{
$sources = [];
foreach ($this->unprocessedConfig as $sourceName => $config) {
if (!empty($sourceName)) {
$configSources = ArrayUtil::fillRecursive($config, $sourceName);
$sources = ArrayUtil::mergeRecursiveDistinct($sources, $configSources);
}
}
return $sources;
}
/**
* Get the configuration to be processed, and clear out the
* 'unprocessed' list.
*
* @return array
*/
protected function fetchUnprocessed()
{
$toBeProcessed = $this->unprocessedConfig;
$this->unprocessedConfig = [];
return $toBeProcessed;
}
/**
* Use a map-reduce to evaluate the items to be processed,
* and merge them into the processed array.
*
* @param array $processed
* @param array $toBeProcessed
* @return array
*/
protected function process(array $processed, array $toBeProcessed, $referenceArray = [])
{
$toBeReduced = array_map([$this, 'preprocess'], $toBeProcessed);
$reduced = array_reduce($toBeReduced, [$this, 'reduceOne'], $processed);
return $this->evaluate($reduced, $referenceArray);
}
/**
* Process a single configuration file from the 'to be processed'
* list. By default this is a no-op. Override this method to
* provide any desired configuration preprocessing, e.g. dot-notation
* expansion of the configuration keys, etc.
*
* @param array $config
* @return array
*/
protected function preprocess(array $config)
{
return $config;
}
/**
* Evaluate one item in the 'to be evaluated' list, and then
* merge it into the processed configuration (the 'carry').
*
* @param array $processed
* @param array $config
* @return array
*/
protected function reduceOne(array $processed, array $config)
{
return ArrayUtil::mergeRecursiveDistinct($processed, $config);
}
/**
* Evaluate one configuration item.
*
* @param array $processed
* @param array $config
* @return array
*/
protected function evaluate(array $config, $referenceArray = [])
{
return $this->expander->expandArrayProperties(
$config,
$referenceArray
);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Consolidation\Config\Loader;
use Symfony\Component\Yaml\Yaml;
/**
* Load configuration files, and fill in any property values that
* need to be expanded.
*/
class YamlConfigLoader extends ConfigLoader
{
public function load($path)
{
$this->setSourceName($path);
// We silently skip any nonexistent config files, so that
// clients may simply `load` all of their candidates.
if (!file_exists($path)) {
$this->config = [];
return $this;
}
$this->config = (array) Yaml::parse(file_get_contents($path));
return $this;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Consolidation\Config\Util;
/**
* Useful array utilities.
*/
class ArrayUtil
{
/**
* Merges arrays recursively while preserving.
*
* @param array $array1
* @param array $array2
*
* @return array
*
* @see http://php.net/manual/en/function.array-merge-recursive.php#92195
* @see https://github.com/grasmash/bolt/blob/robo-rebase/src/Robo/Common/ArrayManipulator.php#L22
*/
public static function mergeRecursiveDistinct(
array &$array1,
array &$array2
) {
$merged = $array1;
foreach ($array2 as $key => &$value) {
$merged[$key] = self::mergeRecursiveValue($merged, $key, $value);
}
return $merged;
}
/**
* Process the value in an mergeRecursiveDistinct - make a recursive
* call if needed.
*/
protected static function mergeRecursiveValue(&$merged, $key, $value)
{
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
return self::mergeRecursiveDistinct($merged[$key], $value);
}
return $value;
}
/**
* Fills all of the leaf-node values of a nested array with the
* provided replacement value.
*/
public static function fillRecursive(array $data, $fill)
{
$result = [];
foreach ($data as $key => $value) {
$result[$key] = $fill;
if (self::isAssociative($value)) {
$result[$key] = self::fillRecursive($value, $fill);
}
}
return $result;
}
/**
* Return true if the provided parameter is an array, and at least
* one key is non-numeric.
*/
public static function isAssociative($testArray)
{
if (!is_array($testArray)) {
return false;
}
foreach (array_keys($testArray) as $key) {
if (!is_numeric($key)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Consolidation\Config\Util;
/**
* Fetch a configuration value from a configuration group. If the
* desired configuration value is not found in the most specific
* group named, keep stepping up to the next parent group until a
* value is located.
*
* Given the following constructor inputs:
* - $prefix = "command."
* - $group = "foo.bar.baz"
* - $postfix = ".options."
* Then the `get` method will then consider, in order:
* - command.foo.bar.baz.options
* - command.foo.bar.options
* - command.foo.options
* If any of these contain an option for "$key", then return its value.
*/
class ConfigFallback extends ConfigGroup
{
/**
* @inheritdoc
*/
public function get($key)
{
return $this->getWithFallback($key, $this->group, $this->prefix, $this->postfix);
}
/**
* Fetch an option value from a given key, or, if that specific key does
* not contain a value, then consult various fallback options until a
* value is found.
*
*/
protected function getWithFallback($key, $group, $prefix = '', $postfix = '.')
{
$configKey = "{$prefix}{$group}${postfix}{$key}";
if ($this->config->has($configKey)) {
return $this->config->get($configKey);
}
if ($this->config->hasDefault($configKey)) {
return $this->config->getDefault($configKey);
}
$moreGeneralGroupname = $this->moreGeneralGroupName($group);
if ($moreGeneralGroupname) {
return $this->getWithFallback($key, $moreGeneralGroupname, $prefix, $postfix);
}
return null;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Consolidation\Config\Util;
/**
* Fetch a configuration value from a configuration group. If the
* desired configuration value is not found in the most specific
* group named, keep stepping up to the next parent group until a
* value is located.
*
* Given the following constructor inputs:
* - $prefix = "command."
* - $group = "foo.bar.baz"
* - $postfix = ".options."
* Then the `get` method will then consider, in order:
* - command.foo.bar.baz.options
* - command.foo.bar.options
* - command.foo.options
* If any of these contain an option for "$key", then return its value.
*/
abstract class ConfigGroup
{
protected $config;
protected $group;
protected $prefix;
protected $postfix;
public function __construct($config, $group, $prefix = '', $postfix = '.')
{
$this->config = $config;
$this->group = $group;
$this->prefix = $prefix;
$this->postfix = $postfix;
}
/**
* Return a description of the configuration group (with prefix and postfix).
*/
public function describe($property)
{
return $this->prefix . $this->group . $this->postfix . $property;
}
/**
* Get the requested configuration key from the most specific configuration
* group that contains it.
*/
abstract public function get($key);
/**
* Given a group name, such as "foo.bar.baz", return the next configuration
* group in the fallback hierarchy, e.g. "foo.bar".
*/
protected function moreGeneralGroupName($group)
{
$result = preg_replace('#\.[^.]*$#', '', $group);
if ($result != $group) {
return $result;
}
return false;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Consolidation\Config\Util;
/**
* Works like 'getWithFallback', but merges results from all applicable
* groups. Settings from most specific group take precedence.
*/
class ConfigMerge extends ConfigGroup
{
/**
* @inheritdoc
*/
public function get($key)
{
return $this->getWithMerge($key, $this->group, $this->prefix, $this->postfix);
}
/**
* Merge available configuration from each configuration group.
*/
public function getWithMerge($key, $group, $prefix = '', $postfix = '.')
{
$configKey = "{$prefix}{$group}${postfix}{$key}";
$result = $this->config->get($configKey, []);
if (!is_array($result)) {
throw new \UnexpectedValueException($configKey . ' must be a list of settings to apply.');
}
$moreGeneralGroupname = $this->moreGeneralGroupName($group);
if ($moreGeneralGroupname) {
$result += $this->getWithMerge($key, $moreGeneralGroupname, $prefix, $postfix);
}
return $result;
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
use Consolidation\Config\ConfigInterface;
/**
* Overlay different configuration objects that implement ConfigInterface
* to make a priority-based, merged configuration object.
*
* Note that using a ConfigOverlay hides the defaults stored in each
* individual configuration context. When using overlays, always call
* getDefault / setDefault on the ConfigOverlay object itself.
*/
class ConfigOverlay implements ConfigInterface
{
protected $contexts = [];
const DEFAULT_CONTEXT = 'default';
const PROCESS_CONTEXT = 'process';
public function __construct()
{
$this->contexts[self::DEFAULT_CONTEXT] = new Config();
$this->contexts[self::PROCESS_CONTEXT] = new Config();
}
/**
* Add a named configuration object to the configuration overlay.
* Configuration objects added LAST have HIGHEST priority, with the
* exception of the fact that the process context always has the
* highest priority.
*
* If a context has already been added, its priority will not change.
*/
public function addContext($name, ConfigInterface $config)
{
$process = $this->contexts[self::PROCESS_CONTEXT];
unset($this->contexts[self::PROCESS_CONTEXT]);
$this->contexts[$name] = $config;
$this->contexts[self::PROCESS_CONTEXT] = $process;
return $this;
}
/**
* Add a placeholder context that will be prioritized higher than
* existing contexts. This is done to ensure that contexts added
* later will maintain a higher priority if the placeholder context
* is later relaced with a different configuration set via addContext().
*
* @param string $name
* @return $this
*/
public function addPlaceholder($name)
{
return $this->addContext($name, new Config());
}
/**
* Increase the priority of the named context such that it is higher
* in priority than any existing context except for the 'process'
* context.
*
* @param string $name
* @return $this
*/
public function increasePriority($name)
{
$config = $this->getContext($name);
unset($this->contexts[$name]);
return $this->addContext($name, $config);
}
public function hasContext($name)
{
return isset($this->contexts[$name]);
}
public function getContext($name)
{
if ($this->hasContext($name)) {
return $this->contexts[$name];
}
return new Config();
}
public function removeContext($name)
{
unset($this->contexts[$name]);
}
/**
* Determine if a non-default config value exists.
*/
public function findContext($key)
{
foreach (array_reverse($this->contexts) as $name => $config) {
if ($config->has($key)) {
return $config;
}
}
return false;
}
/**
* @inheritdoc
*/
public function has($key)
{
return $this->findContext($key) != false;
}
/**
* @inheritdoc
*/
public function get($key, $default = null)
{
$context = $this->findContext($key);
if ($context) {
return $context->get($key, $default);
}
return $default;
}
/**
* @inheritdoc
*/
public function set($key, $value)
{
$this->contexts[self::PROCESS_CONTEXT]->set($key, $value);
return $this;
}
/**
* @inheritdoc
*/
public function import($data)
{
$this->unsupported(__FUNCTION__);
}
/**
* @inheritdoc
*/
public function replace($data)
{
$this->unsupported(__FUNCTION__);
}
/**
* @inheritdoc
*/
public function combine($data)
{
$this->unsupported(__FUNCTION__);
}
/**
* @inheritdoc
*/
protected function unsupported($fn)
{
throw new \Exception("The method '$fn' is not supported for the ConfigOverlay class.");
}
/**
* @inheritdoc
*/
public function export()
{
$export = [];
foreach ($this->contexts as $name => $config) {
$export = array_merge_recursive($export, $config->export());
}
return $export;
}
/**
* @inheritdoc
*/
public function hasDefault($key)
{
return $this->contexts[self::DEFAULT_CONTEXT]->has($key);
}
/**
* @inheritdoc
*/
public function getDefault($key, $default = null)
{
return $this->contexts[self::DEFAULT_CONTEXT]->get($key, $default);
}
/**
* @inheritdoc
*/
public function setDefault($key, $value)
{
$this->contexts[self::DEFAULT_CONTEXT]->set($key, $value);
return $this;
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
use Consolidation\Config\ConfigInterface;
/**
* Provide a configuration object that fetches values from environment
* variables.
*/
class EnvConfig implements ConfigInterface
{
/** @var string */
protected $prefix;
/**
* EnvConfig constructor
*
* @param $prefix The string to appear before every environment
* variable key. For example, if the prefix is 'MYAPP_', then
* the key 'foo.bar' will be fetched from the environment variable
* MYAPP_FOO_BAR.
*/
public function __construct($prefix)
{
// Ensure that the prefix is always uppercase, and always
// ends with a '_', regardless of the form the caller provided.
$this->prefix = strtoupper(rtrim($prefix, '_')) . '_';
}
/**
* @inheritdoc
*/
public function has($key)
{
return $this->get($key) !== null;
}
/**
* @inheritdoc
*/
public function get($key, $defaultFallback = null)
{
$envKey = $this->prefix . strtoupper(strtr($key, '.-', '__'));
$envKey = str_replace($this->prefix . $this->prefix, $this->prefix, $envKey);
return getenv($envKey) ?: $defaultFallback;
}
/**
* @inheritdoc
*/
public function set($key, $value)
{
throw new \Exception('Cannot call "set" on environmental configuration.');
}
/**
* @inheritdoc
*/
public function import($data)
{
// no-op
}
/**
* @inheritdoc
*/
public function export()
{
return [];
}
/**
* @inheritdoc
*/
public function hasDefault($key)
{
return false;
}
/**
* @inheritdoc
*/
public function getDefault($key, $defaultFallback = null)
{
return $defaultFallback;
}
/**
* @inheritdoc
*/
public function setDefault($key, $value)
{
throw new \Exception('Cannot call "setDefault" on environmental configuration.');
}
}

View File

@@ -1 +0,0 @@
../../tests

View File

@@ -0,0 +1,130 @@
<?php
namespace Consolidation\Config\Inject;
use Consolidation\Config\Config;
use Consolidation\TestUtils\MyFooCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;
class ConfigForCommandTest extends \PHPUnit_Framework_TestCase
{
protected $config;
protected function setUp()
{
$data = [
// Global options
'options' => [
'global' => 'from-config',
],
// Define some configuration settings for the options for
// the commands my:foo and my:bar.
'command' => [
'my' => [
// commands.my.options.* apply to all my:* commands.
'options' => [
'dir' => '/etc/common',
'priority' => 'normal',
],
'foo' => [
// commands.my.foo.options.* apply only to the my:foo command.
'options' => [
'name' => 'baz',
],
],
],
],
];
$this->config = new Config($data);
}
public function testInjection()
{
$command = new MyFooCommand();
$input = new StringInput('my:foo');
list($status, $output) = $this->runCommandViaApplication($command, $input);
$expectedOutput = <<< EOT
Enter my:foo
dir: /etc/common
name: baz
other: fish
EOT;
$this->assertEquals(0, $status);
$this->assertEquals($expectedOutput, $output);
}
public function testInjectionWithOverride()
{
$command = new MyFooCommand();
$input = new StringInput('my:foo --name=Fred');
list($status, $output) = $this->runCommandViaApplication($command, $input);
$expectedOutput = <<< EOT
Enter my:foo
dir: /etc/common
name: Fred
other: fish
EOT;
$this->assertEquals(0, $status);
$this->assertEquals($expectedOutput, $output);
}
public function testHelpDefaultInjection()
{
$command = new MyFooCommand();
$input = new StringInput('help my:foo');
list($status, $output) = $this->runCommandViaApplication($command, $input);
$expectedOutput = <<< EOT
What is the name of the thing we are naming [default: "baz"]
EOT;
$this->assertEquals(0, $status);
$this->assertContains($expectedOutput, $output);
$expectedOutput = <<< EOT
A certain global option. [default: "from-config"]
EOT;
$this->assertContains($expectedOutput, $output);
}
protected function runCommandViaApplication($command, $input)
{
$application = new Application('TestApplication', '0.0.0');
$application->getDefinition()
->addOption(
new InputOption('--global', null, InputOption::VALUE_REQUIRED, 'A certain global option.', 'hardcoded')
);
$output = new BufferedOutput();
$configInjector = new ConfigForCommand($this->config);
$configInjector->setApplication($application);
$eventDispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher();
$eventDispatcher->addSubscriber($configInjector);
$application->setDispatcher($eventDispatcher);
$application->setAutoExit(false);
$application->add($command);
$statusCode = $application->run($input, $output);
$commandOutput = trim($output->fetch());
return [$statusCode, $commandOutput];
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Consolidation\Config\Inject;
use Consolidation\Config\Config;
use Consolidation\TestUtils\ApplyConfigTestTarget;
class ConfigForSettersTest extends \PHPUnit_Framework_TestCase
{
public function testApplyConfig()
{
$data = [
// Define some configuration settings for the configuration
// of some task \My\Tasks\Operations\Frobulate.
'task' => [
'Operations' => [
// task.Operations.settings apply to all tasks in
// any *.Tass.Operations namespace.
'settings' => [
'dir' => '/base/dir',
],
'Frobulate' => [
// task.Operations.Frobulate.settings applies only
// the Frobulate task.
'settings' => [
'dir' => '/override/dir',
],
],
],
],
];
$config = new Config($data);
$applicator = new ConfigForSetters($config, 'Operations.Frobulate', 'task.');
$testTarget = new ApplyConfigTestTarget();
$applicator->apply($testTarget, 'settings');
$this->assertEquals('/override/dir', $testTarget->getDir());
$this->assertEquals(null, $testTarget->getBad());
}
public function testApplyBadConfig()
{
$data = [
// Define some configuration settings for the configuration
// of some task \My\Tasks\Operations\Frobulate.
'task' => [
'Operations' => [
// task.Operations.settings apply to all tasks in
// any *.Tass.Operations namespace.
'settings' => [
'dir' => '/base/dir',
],
'Frobulate' => [
// task.Operations.Frobulate.settings applies only
// the Frobulate task.
'settings' => [
'bad' => 'fire truck',
],
],
],
],
];
$config = new Config($data);
$applicator = new ConfigForSetters($config, 'Operations.Frobulate', 'task.');
$testTarget = new ApplyConfigTestTarget();
$exceptionMessage = '';
try
{
$applicator->apply($testTarget, 'settings');
}
catch (\Exception $e)
{
$exceptionMessage = $e->getMessage();
}
// We would prefer it if bad methods were never called; unfortunately,
// declaring the return type of a method cannot be done in a reliable
// way (via reflection) until php 7, so we allow these methods to be
// called for now.
$this->assertEquals('fire truck', $testTarget->getBad());
$this->assertEquals('Consolidation\\TestUtils\\ApplyConfigTestTarget::bad did not return \'$this\' when processing task.Operations.Frobulate.settings.', $exceptionMessage);
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
class ConfigGroupTest extends \PHPUnit_Framework_TestCase
{
protected $config;
protected function setUp()
{
$data = [
// Define some configuration settings for the options for
// the commands my:foo and my:bar.
'command' => [
'my' => [
// commands.my.options.* apply to all my:* commands.
'options' => [
'path' => '/etc/common',
'priority' => 'normal',
],
'foo' => [
// commands.my.foo.options.* apply only to the my:foo command.
'options' => [
'name' => 'baz',
],
],
'bar' => [
// Similarly, commands.my.bar.options is for the my:bar command.
'options' => [
'priority' => 'high',
],
],
],
],
// Define some configuration settings for the configuration
// of some task \My\Tasks\Operations\Frobulate.
'task' => [
'Operations' => [
// task.Operations.settings apply to all tasks in
// any *.Tass.Operations namespace.
'settings' => [
'dir' => '/base/dir',
],
'Frobulate' => [
// task.Operations.Frobulate.settings applies only
// the Frobulate task.
'settings' => [
'object' => 'fire truck',
],
],
],
],
];
$this->config = new Config($data);
}
public function testDotNotation()
{
// Test the test
$this->assertEquals('baz', $this->config->get('command.my.foo.options.name'));
}
public function testFallback()
{
$fooFallback = new ConfigFallback($this->config, 'my.foo', 'command.', '.options.');
$barFallback = new ConfigFallback($this->config, 'my.bar', 'command.', '.options.');
$this->assertEquals(null, $barFallback->get('name'));
$this->assertEquals('baz', $fooFallback->get('name'));
$this->assertEquals('high', $barFallback->get('priority'));
$this->assertEquals('normal', $fooFallback->get('priority'));
$this->assertEquals('/etc/common', $barFallback->get('path'));
$this->assertEquals('/etc/common', $fooFallback->get('path'));
}
public function testMerge()
{
$frobulateMerge = new ConfigMerge($this->config, 'Operations.Frobulate', 'task.');
$settings = $frobulateMerge->get('settings');
$this->assertEquals('fire truck', $settings['object']);
$this->assertEquals('/base/dir', $settings['dir']);
$keys = array_keys($settings);
sort($keys);
$this->assertEquals('dir,object', implode(',', $keys));
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Consolidation\Config\Loader;
class ConfigLoaderTest extends \PHPUnit_Framework_TestCase
{
public function testConfigLoader()
{
$loader = new YamlConfigLoader();
// Assert that our test data exists (test the test)
$path = __DIR__ . '/data/config-1.yml';
$this->assertTrue(file_exists($path));
$loader->load($path);
$configFile = basename($loader->getSourceName());
$this->assertEquals('config-1.yml', $configFile);
// Make sure that the data we loaded contained the expected keys
$keys = $loader->keys();
sort($keys);
$keysString = implode(',', $keys);
$this->assertEquals('c,m', $keysString);
$configData = $loader->export();
$this->assertEquals('foo', $configData['c']);
$this->assertEquals('1', $configData['m'][0]);
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
use Consolidation\Config\Loader\ConfigProcessor;
use Consolidation\Config\Loader\YamlConfigLoader;
class ConfigOverlayTest extends \PHPUnit_Framework_TestCase
{
protected $overlay;
protected function setUp()
{
$aliasContext = new Config();
$aliasContext->import([
'hidden-by-a' => 'alias hidden-by-a',
'hidden-by-process' => 'alias hidden-by-process',
'options' =>[
'a-a' => 'alias-a',
],
'command' => [
'foo' => [
'bar' => [
'command' => [
'options' => [
'a-b' => 'alias-b',
],
],
],
],
],
]);
$configFileContext = new Config();
$configFileContext->import([
'hidden-by-cf' => 'config-file hidden-by-cf',
'hidden-by-a' => 'config-file hidden-by-a',
'hidden-by-process' => 'config-file hidden-by-process',
'options' =>[
'cf-a' => 'config-file-a',
],
'command' => [
'foo' => [
'bar' => [
'command' => [
'options' => [
'cf-b' => 'config-file-b',
],
],
],
],
],
]);
$this->overlay = new ConfigOverlay();
$this->overlay->set('hidden-by-process', 'process-h');
$this->overlay->addContext('cf', $configFileContext);
$this->overlay->addContext('a', $aliasContext);
$this->overlay->setDefault('df-a', 'default');
$this->overlay->setDefault('hidden-by-a', 'default hidden-by-a');
$this->overlay->setDefault('hidden-by-cf', 'default hidden-by-cf');
$this->overlay->setDefault('hidden-by-process', 'default hidden-by-process');
}
public function testGetPriority()
{
$this->assertEquals('process-h', $this->overlay->get('hidden-by-process'));
$this->assertEquals('config-file hidden-by-cf', $this->overlay->get('hidden-by-cf'));
$this->assertEquals('alias hidden-by-a', $this->overlay->get('hidden-by-a'));
}
public function testDefault()
{
$this->assertEquals('alias-a', $this->overlay->get('options.a-a'));
$this->assertEquals('alias-a', $this->overlay->get('options.a-a', 'ignored'));
$this->assertEquals('default', $this->overlay->getDefault('df-a', 'ignored'));
$this->assertEquals('nsv', $this->overlay->getDefault('a-a', 'nsv'));
$this->overlay->setDefault('df-a', 'new value');
$this->assertEquals('new value', $this->overlay->getDefault('df-a', 'ignored'));
}
public function testExport()
{
$data = $this->overlay->export();
$this->assertEquals('config-file-a', $data['options']['cf-a']);
$this->assertEquals('alias-a', $data['options']['a-a']);
}
/**
* @expectedException Exception
*/
public function testImport()
{
$data = $this->overlay->import(['a' => 'value']);
}
public function testMaintainPriority()
{
// Get and re-add the 'cf' context. Its priority should not change.
$configFileContext = $this->overlay->getContext('cf');
$this->overlay->addContext('cf', $configFileContext);
// These asserts are the same as in testGetPriority
$this->assertEquals('process-h', $this->overlay->get('hidden-by-process'));
$this->assertEquals('config-file hidden-by-cf', $this->overlay->get('hidden-by-cf'));
$this->assertEquals('alias hidden-by-a', $this->overlay->get('hidden-by-a'));
}
public function testChangePriority()
{
// Increase the priority of the 'cf' context. Now, it should have a higher
// priority than the 'alias' context, but should still have a lower
// priority than the 'process' context.
$this->overlay->increasePriority('cf');
// These asserts are the same as in testGetPriority
$this->assertEquals('process-h', $this->overlay->get('hidden-by-process'));
$this->assertEquals('config-file hidden-by-cf', $this->overlay->get('hidden-by-cf'));
// This one has changed: the config-file value is now found instead
// of the alias value.
$this->assertEquals('config-file hidden-by-a', $this->overlay->get('hidden-by-a'));
}
public function testPlaceholder()
{
$this->overlay->addPlaceholder('lower');
$higherContext = new Config();
$higherContext->import(['priority-test' => 'higher']);
$lowerContext = new Config();
$lowerContext->import(['priority-test' => 'lower']);
// Usually 'lower' would have the highest priority, since it is
// added last. However, our earlier call to 'addPlaceholder' reserves
// a spot for it, so the 'higher' context will end up with a higher
// priority.
$this->overlay->addContext('higher', $higherContext);
$this->overlay->addContext('lower', $lowerContext);
$this->assertEquals('higher', $this->overlay->get('priority-test', 'neither'));
// Test to see that we can change the value of the 'higher' context,
// and the change will be reflected in the overlay.
$higherContext->set('priority-test', 'changed');
$this->assertEquals('changed', $this->overlay->get('priority-test', 'neither'));
// Test to see that the 'process' context still has the highest priority.
$this->overlay->set('priority-test', 'process');
$higherContext->set('priority-test', 'ignored');
$this->assertEquals('process', $this->overlay->get('priority-test', 'neither'));
}
public function testDoesNotHave()
{
$context = $this->overlay->getContext('no-such-context');
$data = $context->export();
$this->assertEquals('[]', json_encode($data));
$this->assertTrue(!$this->overlay->has('no-such-key'));
$this->assertTrue(!$this->overlay->hasDefault('no-such-default'));
$this->assertEquals('no-such-value', $this->overlay->get('no-such-key', 'no-such-value'));
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace Consolidation\Config\Loader;
use Consolidation\TestUtils\TestLoader;
class ConfigProcessorTest extends \PHPUnit_Framework_TestCase
{
public function testConfigProcessorAdd()
{
$config1 = [
'c' => 'foo',
'm' => [1],
];
$config2 = [
'b' => '${c}bar',
'm' => [2],
];
$config3 = [
'a' => '${b}baz',
'm' => [3],
];
$processor = new ConfigProcessor();
$processor->add($config1);
$processor->add($config2);
$processor->add($config3);
$data = $processor->export();
$this->assertEquals('foo', $data['c']);
$this->assertEquals('foobar', $data['b']);
$this->assertEquals('foobarbaz', $data['a']);
}
public function processorForConfigMergeTest($provideSourceNames)
{
$config1 = [
'm' => [
'x' => 'x-1',
'y' => [
'r' => 'r-1',
's' => 's-1',
't' => 't-1',
],
'z' => 'z-1',
],
];
$config2 = [
'm' => [
'w' => 'w-2',
'y' => [
'q' => 'q-2',
's' => 's-2',
],
'z' => 'z-2',
],
];
$config3 = [
'm' => [
'v' => 'v-3',
'y' => [
't' => 't-3',
'u' => 'u-3',
],
'z' => 'z-3',
],
];
$processor = new ConfigProcessor();
$testLoader = new TestLoader();
$testLoader->set($config1);
$testLoader->setSourceName($provideSourceNames ? 'c-1' : '');
$processor->extend($testLoader);
$testLoader->set($config2);
$testLoader->setSourceName($provideSourceNames ? 'c-2' : '');
$processor->extend($testLoader);
$testLoader->set($config3);
$testLoader->setSourceName($provideSourceNames ? 'c-3' : '');
$processor->extend($testLoader);
return $processor;
}
public function testConfigProcessorMergeAssociative()
{
$processor = $this->processorForConfigMergeTest(false);
$data = $processor->export();
$this->assertEquals('{"m":{"x":"x-1","y":{"r":"r-1","s":"s-2","t":"t-3","q":"q-2","u":"u-3"},"z":"z-3","w":"w-2","v":"v-3"}}', json_encode($data));
}
public function testConfigProcessorMergeAssociativeWithSourceNames()
{
$processor = $this->processorForConfigMergeTest(true);
$sources = $processor->sources();
$data = $processor->export();
$this->assertEquals('{"m":{"x":"x-1","y":{"r":"r-1","s":"s-2","t":"t-3","q":"q-2","u":"u-3"},"z":"z-3","w":"w-2","v":"v-3"}}', json_encode($data));
$this->assertEquals('c-1', $sources['m']['x']);
$this->assertEquals('c-1', $sources['m']['y']['r']);
$this->assertEquals('c-2', $sources['m']['w']);
$this->assertEquals('c-2', $sources['m']['y']['s']);
$this->assertEquals('c-3', $sources['m']['z']);
$this->assertEquals('c-3', $sources['m']['y']['u']);
}
public function testConfiProcessorSources()
{
$processor = new ConfigProcessor();
$loader = new YamlConfigLoader();
$processor->extend($loader->load(__DIR__ . '/data/config-1.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-2.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-3.yml'));
$sources = $processor->sources();
$data = $processor->export();
$this->assertEquals('foo', $data['c']);
$this->assertEquals('foobar', $data['b']);
$this->assertEquals('foobarbaz', $data['a']);
$this->assertEquals('3', $data['m'][0]);
$this->assertEquals( __DIR__ . '/data/config-3.yml', $sources['a']);
$this->assertEquals( __DIR__ . '/data/config-2.yml', $sources['b']);
$this->assertEquals( __DIR__ . '/data/config-1.yml', $sources['c']);
$this->assertEquals( __DIR__ . '/data/config-3.yml', $sources['m']);
}
public function testConfiProcessorSourcesLoadInReverseOrder()
{
$processor = new ConfigProcessor();
$loader = new YamlConfigLoader();
$processor->extend($loader->load(__DIR__ . '/data/config-3.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-2.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-1.yml'));
$sources = $processor->sources();
$data = $processor->export();
$this->assertEquals('foo', $data['c']);
$this->assertEquals('foobar', $data['b']);
$this->assertEquals('foobarbaz', $data['a']);
$this->assertEquals('1', $data['m'][0]);
$this->assertEquals( __DIR__ . '/data/config-3.yml', $sources['a']);
$this->assertEquals( __DIR__ . '/data/config-2.yml', $sources['b']);
$this->assertEquals( __DIR__ . '/data/config-1.yml', $sources['c']);
$this->assertEquals( __DIR__ . '/data/config-1.yml', $sources['m']);
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Consolidation\Config;
use Consolidation\Config\Loader\ConfigProcessor;
use Consolidation\Config\Loader\YamlConfigLoader;
class ConfigTest extends \PHPUnit_Framework_TestCase
{
public function testSetters()
{
// Pointless tests just to ensure everything is covered.
$config = new Config();
$config->set('foo', 'bar');
$data = $config->export();
$this->assertEquals('{"foo":"bar"}', json_encode($data));
}
public function testCombine()
{
// Pointless tests just to ensure everything is covered.
$config = new Config();
$config->set('foo', 'bar');
$config->set('baz', 'boz');
$config2 = new Config();
$config2->set('foo', 'fu');
$config2->set('new', 'blue');
$config->combine($config2->export());
$this->assertEquals('fu', $config->get('foo'));
$this->assertEquals('boz', $config->get('baz'));
$this->assertEquals('blue', $config->get('new'));
}
public function testDefault()
{
$data = [
'a' => 'foo',
'b' => 'bar',
'c' => 'boz',
];
$foo = ["foo" => "bar"];
$config = new Config($data);
$config->setDefault('c', 'other');
$config->setDefault('d', 'other');
$config->setDefault('f', $foo);
$this->assertEquals('foo', $config->get('a'));
$this->assertEquals('boz', $config->get('c'));
$this->assertEquals('other', $config->get('d'));
$this->assertEquals('other', $config->getDefault('c'));
$this->assertEquals('', $config->get('e'));
$this->assertEquals('bar', $config->get('f.foo'));
$this->assertEquals('{"foo":"bar"}', json_encode($config->get('f')));
}
public function testDefaultsArray()
{
$data = ['a' => 'foo', 'b' => 'bar', 'c' => 'boz',];
$defaults = ['d' => 'foo', 'e' => 'bar', 'f' => 'boz',];
// Create reflection class to test private methods
$configClass = new \ReflectionClass("Consolidation\Config\Config");
// $defaults
$defaultsProperty = $configClass->getProperty("defaults");
$defaultsProperty->setAccessible(true);
// $getDefaults
$getDefaultsMethod = $configClass->getMethod("getDefaults");
$getDefaultsMethod->setAccessible(true);
// Test the config class
$config = new Config($data);
// Set $config::defaults to an array to test getter and setter
$defaultsProperty->setValue($config, $defaults);
$this->assertTrue(is_array($defaultsProperty->getValue($config)));
$this->assertInstanceOf('Dflydev\DotAccessData\Data',
$getDefaultsMethod->invoke($config));
// Set $config::defaults to a string to test exception
$defaultsProperty->setValue($config, "foo.bar");
$this->setExpectedException("Exception");
$getDefaultsMethod->invoke($config);
}
public function testConfigurationWithCrossFileReferences()
{
$config = new Config();
$processor = new ConfigProcessor();
$loader = new YamlConfigLoader();
$processor->extend($loader->load(__DIR__ . '/data/config-1.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-2.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-3.yml'));
// Does not fail if configuration file cannot be found
$processor->extend($loader->load(__DIR__ . '/data/no-such-file.yml'));
// We must capture the sources before exporting, as export
// dumps this information.
$sources = $processor->sources();
$config->import($processor->export());
$this->assertEquals(implode(',', $config->get('m')), '3');
$this->assertEquals($config->get('a'), 'foobarbaz');
$this->assertEquals($sources['a'], __DIR__ . '/data/config-3.yml');
$this->assertEquals($sources['b'], __DIR__ . '/data/config-2.yml');
$this->assertEquals($sources['c'], __DIR__ . '/data/config-1.yml');
}
public function testConfigurationWithReverseOrderCrossFileReferences()
{
$config = new Config();
$processor = new ConfigProcessor();
$loader = new YamlConfigLoader();
$processor->extend($loader->load(__DIR__ . '/data/config-3.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-2.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-1.yml'));
$sources = $processor->sources();
$config->import($processor->export());
$this->assertEquals(implode(',', $config->get('m')), '1');
if (strpos($config->get('a'), '$') !== false) {
throw new \PHPUnit_Framework_SkippedTestError(
'Evaluation of cross-file references in reverse order not supported.'
);
}
$this->assertEquals($config->get('a'), 'foobarbaz');
$this->assertEquals($sources['a'], __DIR__ . '/data/config-3.yml');
$this->assertEquals($sources['b'], __DIR__ . '/data/config-2.yml');
$this->assertEquals($sources['c'], __DIR__ . '/data/config-1.yml');
}
}

View File

@@ -0,0 +1,3 @@
c: foo
m:
- 1

View File

@@ -0,0 +1,3 @@
b: ${c}bar
m:
- 2

View File

@@ -0,0 +1,3 @@
a: ${b}baz
m:
- 3

View File

@@ -0,0 +1,23 @@
#!/bin/bash
SCENARIO=$1
ACTION=${2-install}
dir=dependencies/${SCENARIO}
if [ -z "$SCENARIO" ] ; then
SCENARIO=default
dir=.
fi
if [ ! -d "$dir" ] ; then
echo "Requested scenario '${SCENARIO}' does not exist."
exit 1
fi
echo "Switch to ${SCENARIO} scenario"
set -ex
composer -n --working-dir=$dir ${ACTION} --prefer-dist --no-scripts
composer -n --working-dir=$dir info

View File

@@ -0,0 +1,66 @@
#!/bin/bash
#
# This script is called automatically on every `composer update`.
# See "post-update-cmd" in the "scripts" section of composer.json.
#
# This script will create a derived composer.json / composer.lock
# pair for every test scenario. Test scenarios are defined in the
# "scenarios" file, which should be customized to suit the needs
# of the project.
#
SELF_DIRNAME="`dirname -- "$0"`"
source ${SELF_DIRNAME}/scenarios
echo
echo "::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"
echo "::"
echo ":: Update dependencies for the following scenarios:"
echo "::"
echo ":: ${SCENARIOS}"
echo "::"
echo "::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"
echo
set -ex
for SCENARIO in ${SCENARIOS} ; do
dir=dependencies/${SCENARIO}
# Define indirect variable names
stability_variable="stability_${SCENARIO}"
requirement_variable="requirement_${SCENARIO}"
platform_php_variable="platform_php_${SCENARIO}"
echo "### Create $dir/composer.json for ${SCENARIO} scenario"
mkdir -p $dir
cp composer.json $dir
# Then set our own platform php version if applicable (otherwise unset it)
composer -n --working-dir=$dir config platform.php "${!platform_php_variable---unset}"
# Temporarily set our vendor directory to 'vendor'
composer -n --working-dir=$dir config vendor-dir vendor
# Set an appropriate minimum stability for this version of Symfony
composer -n --working-dir=$dir config minimum-stability "${!stability_variable-stable}"
# Add a constraint to limit the Symfony version
composer -n --working-dir=$dir require --dev --no-update "${!requirement_variable}"
# Create the composer.lock file. Ignore the vendor directory created.
composer -n --working-dir=$dir update --no-scripts
# Set the vendor directory to its final desired location.
composer -n --working-dir=$dir config vendor-dir '../../vendor'
# The 'autoload' section specifies directory paths that are relative
# to the composer.json file. We will drop in some symlinks so that
# these paths will resolve as if the composer.json were in the root.
for target in $AUTOLOAD_DIRECTORIES ; do
ln -s -f ../../$target $dir
done
done

View File

@@ -0,0 +1,12 @@
#!/bin/bash
SCENARIOS="symfony2 symfony3 symfony4"
AUTOLOAD_DIRECTORIES='src tests'
platform_php_symfony2='5.4'
platform_php_symfony3='5.6'
requirement_symfony2='symfony/console:^2.8'
requirement_symfony3='symfony/console:^3'
requirement_symfony4='symfony/console:^4'

View File

@@ -0,0 +1,43 @@
<?php
namespace Consolidation\TestUtils;
class ApplyConfigTestTarget
{
protected $dir;
protected $value;
/**
* A proper setter for the 'dir' property
*/
public function dir($dir)
{
$this->dir = $dir;
return $this;
}
/**
* A getter for the 'dir' property that we will use to
* determine if the setter was called.
*/
public function getDir()
{
return $this->dir;
}
/**
* A bad setter that does not return $this.
*/
public function bad($value)
{
$this->value = $value;
}
/**
* A getter for the bad setter.
*/
public function getBad()
{
return $this->value;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Consolidation\TestUtils;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
class MyFooCommand extends Command
{
protected function configure()
{
$this
->setName('my:foo')
->setDescription('My foo command.')
->setHelp('This command tests command option injection by echoing its options')
->addOption(
'other',
null,
InputOption::VALUE_REQUIRED,
'Some other option',
'fish'
)
->addOption(
'name',
null,
InputOption::VALUE_REQUIRED,
'What is the name of the thing we are naming',
'George'
)
->addOption(
'dir',
null,
InputOption::VALUE_REQUIRED,
'What is the base directory to use for this command',
'/default/path'
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln('Enter my:foo');
$output->writeln('dir: ' . $input->getOption('dir'));
$output->writeln('name: ' . $input->getOption('name'));
$output->writeln('other: ' . $input->getOption('other'));
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Consolidation\TestUtils;
use Consolidation\Config\Loader\ConfigLoaderInterface;
class TestLoader implements ConfigLoaderInterface
{
protected $data;
protected $sourceName;
public function set($data)
{
$this->data = $data;
}
public function setSourceName($name)
{
$this->sourceName = $name;
}
public function export()
{
return $this->data;
}
public function keys()
{
return array_keys($this->data);
}
public function getSourceName()
{
return $this->sourceName;
}
}

View File

@@ -1 +0,0 @@
../../src

View File

@@ -0,0 +1,157 @@
<?php
namespace Consolidation\Config;
use Dflydev\DotAccessData\Data;
class Config implements ConfigInterface
{
/**
* @var Data
*/
protected $config;
/**
* TODO: make this private in 2.0 to prevent being saved as an array
* Making private now breaks backward compatibility
*
* @var Data
*/
protected $defaults;
/**
* Create a new configuration object, and initialize it with
* the provided nested array containing configuration data.
*
* @param array $data - Config data to store
*/
public function __construct(array $data = null)
{
$this->config = new Data($data);
$this->setDefaults(new Data());
}
/**
* {@inheritdoc}
*/
public function has($key)
{
return ($this->config->has($key));
}
/**
* {@inheritdoc}
*/
public function get($key, $defaultFallback = null)
{
if ($this->has($key)) {
return $this->config->get($key);
}
return $this->getDefault($key, $defaultFallback);
}
/**
* {@inheritdoc}
*/
public function set($key, $value)
{
$this->config->set($key, $value);
return $this;
}
/**
* {@inheritdoc}
*/
public function import($data)
{
return $this->replace($data);
}
/**
* {@inheritdoc}
*/
public function replace($data)
{
$this->config = new Data($data);
return $this;
}
/**
* {@inheritdoc}
*/
public function combine($data)
{
if (!empty($data)) {
$this->config->import($data, true);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function export()
{
return $this->config->export();
}
/**
* {@inheritdoc}
*/
public function hasDefault($key)
{
return $this->getDefaults()->has($key);
}
/**
* {@inheritdoc}
*/
public function getDefault($key, $defaultFallback = null)
{
return $this->hasDefault($key) ? $this->getDefaults()->get($key) : $defaultFallback;
}
/**
* {@inheritdoc}
*/
public function setDefault($key, $value)
{
$this->getDefaults()->set($key, $value);
return $this;
}
/**
* Return the class $defaults property and ensure it's a Data object
* TODO: remove Data object validation in 2.0
*
* @return Data
*/
protected function getDefaults()
{
// Ensure $this->defaults is a Data object (not an array)
if (!$this->defaults instanceof Data) {
$this->setDefaults($this->defaults);
}
return $this->defaults;
}
/**
* Sets the $defaults class parameter
* TODO: remove support for array in 2.0 as this would currently break backward compatibility
*
* @param Data|array $defaults
*
* @throws \Exception
*/
protected function setDefaults($defaults)
{
if (is_array($defaults)) {
$this->defaults = new Data($defaults);
} elseif ($defaults instanceof Data) {
$this->defaults = $defaults;
} else {
throw new \Exception("Unknown type provided for \$defaults");
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Consolidation\Config;
interface ConfigInterface
{
/**
* Determine if a non-default config value exists.
*/
public function has($key);
/**
* Fetch a configuration value
*
* @param string $key Which config item to look up
* @param string|null $defaultFallback Fallback default value to use when
* configuration object has neither a value nor a default. Use is
* discouraged; use default context in ConfigOverlay, or provide defaults
* using a config processor.
*
* @return mixed
*/
public function get($key, $defaultFallback = null);
/**
* Set a config value
*
* @param string $key
* @param mixed $value
*
* @return $this
*/
public function set($key, $value);
/**
* Import configuration from the provided nexted array, replacing whatever
* was here previously. No processing is done on the provided data.
*
* @deprecated Use 'replace'. Dflydev\DotAccessData\Data::import() merges, which is confusing, since this method replaces.
*
* @param array $data
* @return Config
*/
public function import($data);
/**
* Load configuration from the provided nexted array, replacing whatever
* was here previously. No processing is done on the provided data.
*
* TODO: This will become a required method in version 2.0. Adding now
* would break clients that implement ConfigInterface.
*
* @param array $data
* @return Config
*/
// public function replace($data);
/**
* Import configuration from the provided nexted array, merging with whatever
* was here previously. No processing is done on the provided data.
* Any data provided to the combine() method will overwrite existing data
* with the same key.
*
* TODO: This will become a required method in version 2.0. Adding now
* would break clients that implement ConfigInterface.
*
* @param array $data
* @return Config
*/
// public function combine($data);
/**
* Export all configuration as a nested array.
*/
public function export();
/**
* Return the default value for a given configuration item.
*
* @param string $key
*
* @return mixed
*/
public function hasDefault($key);
/**
* Return the default value for a given configuration item.
*
* @param string $key
* @param mixed $defaultFallback
*
* @return mixed
*/
public function getDefault($key, $defaultFallback = null);
/**
* Set the default value for a configuration setting. This allows us to
* set defaults either before or after more specific configuration values
* are loaded. Keeping defaults separate from current settings also
* allows us to determine when a setting has been overridden.
*
* @param string $key
* @param string $value
*/
public function setDefault($key, $value);
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Consolidation\Config;
interface GlobalOptionDefaultValuesInterface
{
/**
* Return an associative array of option-key => default-value
*/
public function getGlobalOptionDefaultValues();
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Consolidation\Config\Inject;
use Consolidation\Config\ConfigInterface;
use Consolidation\Config\Util\ConfigFallback;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputOption;
class ConfigForCommand implements EventSubscriberInterface
{
protected $config;
protected $application;
public function __construct(ConfigInterface $config)
{
$this->config = $config;
}
public function setApplication(Application $application)
{
$this->application = $application;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [ConsoleEvents::COMMAND => 'injectConfiguration'];
}
/**
* Before a Console command runs, inject configuration settings
* for this command into the default value of the options of
* this command.
*
* @param \Symfony\Component\Console\Event\ConsoleCommandEvent $event
*/
public function injectConfiguration(ConsoleCommandEvent $event)
{
$command = $event->getCommand();
$this->injectConfigurationForGlobalOptions($event->getInput());
$this->injectConfigurationForCommand($command, $event->getInput());
$targetOfHelpCommand = $this->getHelpCommandTarget($command, $event->getInput());
if ($targetOfHelpCommand) {
$this->injectConfigurationForCommand($targetOfHelpCommand, $event->getInput());
}
}
protected function injectConfigurationForGlobalOptions($input)
{
if (!$this->application) {
return;
}
$configGroup = new ConfigFallback($this->config, 'options');
$definition = $this->application->getDefinition();
$options = $definition->getOptions();
return $this->injectConfigGroupIntoOptions($configGroup, $options, $input);
}
protected function injectConfigurationForCommand($command, $input)
{
$commandName = $command->getName();
$commandName = str_replace(':', '.', $commandName);
$configGroup = new ConfigFallback($this->config, $commandName, 'command.', '.options.');
$definition = $command->getDefinition();
$options = $definition->getOptions();
return $this->injectConfigGroupIntoOptions($configGroup, $options, $input);
}
protected function injectConfigGroupIntoOptions($configGroup, $options, $input)
{
foreach ($options as $option => $inputOption) {
$key = str_replace('.', '-', $option);
$value = $configGroup->get($key);
if ($value !== null) {
if (is_bool($value) && ($value == true)) {
$input->setOption($key, $value);
} elseif ($inputOption->acceptValue()) {
$inputOption->setDefault($value);
}
}
}
}
protected function getHelpCommandTarget($command, $input)
{
if (($command->getName() != 'help') || (!isset($this->application))) {
return false;
}
$this->fixInputForSymfony2($command, $input);
// Symfony Console helpfully swaps 'command_name' and 'command'
// depending on whether the user entered `help foo` or `--help foo`.
// One of these is always `help`, and the other is the command we
// are actually interested in.
$nameOfCommandToDescribe = $input->getArgument('command_name');
if ($nameOfCommandToDescribe == 'help') {
$nameOfCommandToDescribe = $input->getArgument('command');
}
return $this->application->find($nameOfCommandToDescribe);
}
protected function fixInputForSymfony2($command, $input)
{
// Symfony 3.x prepares $input for us; Symfony 2.x, on the other
// hand, passes it in prior to binding with the command definition,
// so we have to go to a little extra work. It may be inadvisable
// to do these steps for commands other than 'help'.
if (!$input->hasArgument('command_name')) {
$command->ignoreValidationErrors();
$command->mergeApplicationDefinition();
$input->bind($command->getDefinition());
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Consolidation\Config\Inject;
use Consolidation\Config\Util\ConfigMerge;
/**
* Given an object that contains configuration methods, inject any
* configuration found in the configuration file.
*
* The proper use for this method is to call setter methods of the
* provided object. Using configuration to call methods that do work
* is an abuse of this mechanism.
*/
class ConfigForSetters
{
protected $config;
public function __construct($config, $group, $prefix = '', $postfix = '')
{
if (!empty($group) && empty($postfix)) {
$postfix = '.';
}
$this->config = new ConfigMerge($config, $group, $prefix, $postfix);
}
public function apply($object, $configurationKey)
{
$settings = $this->config->get($configurationKey);
foreach ($settings as $setterMethod => $args) {
$fn = [$object, $setterMethod];
if (is_callable($fn)) {
$result = call_user_func_array($fn, (array)$args);
// We require that $fn must only be used with setter methods.
// Setter methods are required to always return $this so that
// they may be chained. We will therefore throw an exception
// for any setter that returns something else.
if ($result != $object) {
$methodDescription = get_class($object) . "::$setterMethod";
$propertyDescription = $this->config->describe($configurationKey);
throw new \Exception("$methodDescription did not return '\$this' when processing $propertyDescription.");
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Consolidation\Config\Loader;
/**
* Load configuration files.
*/
abstract class ConfigLoader implements ConfigLoaderInterface
{
protected $config = [];
protected $source = '';
public function getSourceName()
{
return $this->source;
}
protected function setSourceName($source)
{
$this->source = $source;
return $this;
}
public function export()
{
return $this->config;
}
public function keys()
{
return array_keys($this->config);
}
abstract public function load($path);
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Consolidation\Config\Loader;
/**
* Load configuration files, and fill in any property values that
* need to be expanded.
*/
interface ConfigLoaderInterface
{
/**
* Convert loaded configuration into a simple php nested array.
*
* @return array
*/
public function export();
/**
* Return the top-level keys in the exported data.
*
* @return array
*/
public function keys();
/**
* Return a symbolic name for this configuration loader instance.
*/
public function getSourceName();
}

View File

@@ -0,0 +1,167 @@
<?php
namespace Consolidation\Config\Loader;
use Grasmash\Expander\Expander;
use Consolidation\Config\Util\ArrayUtil;
/**
* The config processor combines multiple configuration
* files together, and processes them as necessary.
*/
class ConfigProcessor
{
protected $processedConfig = [];
protected $unprocessedConfig = [];
protected $expander;
public function __construct($expander = null)
{
$this->expander = $expander ?: new Expander();
}
/**
* Extend the configuration to be processed with the
* configuration provided by the specified loader.
*
* @param ConfigLoaderInterface $loader
*/
public function extend(ConfigLoaderInterface $loader)
{
return $this->addFromSource($loader->export(), $loader->getSourceName());
}
/**
* Extend the configuration to be processed with
* the provided nested array.
*
* @param array $data
*/
public function add($data)
{
$this->unprocessedConfig[] = $data;
return $this;
}
/**
* Extend the configuration to be processed with
* the provided nested array. Also record the name
* of the data source, if applicable.
*
* @param array $data
* @param string $source
*/
protected function addFromSource($data, $source = '')
{
if (empty($source)) {
return $this->add($data);
}
$this->unprocessedConfig[$source] = $data;
return $this;
}
/**
* Process all of the configuration that has been collected,
* and return a nested array.
*
* @return array
*/
public function export($referenceArray = [])
{
if (!empty($this->unprocessedConfig)) {
$this->processedConfig = $this->process(
$this->processedConfig,
$this->fetchUnprocessed(),
$referenceArray
);
}
return $this->processedConfig;
}
/**
* To aid in debugging: return the source of each configuration item.
* n.b. Must call this function *before* export and save the result
* if persistence is desired.
*/
public function sources()
{
$sources = [];
foreach ($this->unprocessedConfig as $sourceName => $config) {
if (!empty($sourceName)) {
$configSources = ArrayUtil::fillRecursive($config, $sourceName);
$sources = ArrayUtil::mergeRecursiveDistinct($sources, $configSources);
}
}
return $sources;
}
/**
* Get the configuration to be processed, and clear out the
* 'unprocessed' list.
*
* @return array
*/
protected function fetchUnprocessed()
{
$toBeProcessed = $this->unprocessedConfig;
$this->unprocessedConfig = [];
return $toBeProcessed;
}
/**
* Use a map-reduce to evaluate the items to be processed,
* and merge them into the processed array.
*
* @param array $processed
* @param array $toBeProcessed
* @return array
*/
protected function process(array $processed, array $toBeProcessed, $referenceArray = [])
{
$toBeReduced = array_map([$this, 'preprocess'], $toBeProcessed);
$reduced = array_reduce($toBeReduced, [$this, 'reduceOne'], $processed);
return $this->evaluate($reduced, $referenceArray);
}
/**
* Process a single configuration file from the 'to be processed'
* list. By default this is a no-op. Override this method to
* provide any desired configuration preprocessing, e.g. dot-notation
* expansion of the configuration keys, etc.
*
* @param array $config
* @return array
*/
protected function preprocess(array $config)
{
return $config;
}
/**
* Evaluate one item in the 'to be evaluated' list, and then
* merge it into the processed configuration (the 'carry').
*
* @param array $processed
* @param array $config
* @return array
*/
protected function reduceOne(array $processed, array $config)
{
return ArrayUtil::mergeRecursiveDistinct($processed, $config);
}
/**
* Evaluate one configuration item.
*
* @param array $processed
* @param array $config
* @return array
*/
protected function evaluate(array $config, $referenceArray = [])
{
return $this->expander->expandArrayProperties(
$config,
$referenceArray
);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Consolidation\Config\Loader;
use Symfony\Component\Yaml\Yaml;
/**
* Load configuration files, and fill in any property values that
* need to be expanded.
*/
class YamlConfigLoader extends ConfigLoader
{
public function load($path)
{
$this->setSourceName($path);
// We silently skip any nonexistent config files, so that
// clients may simply `load` all of their candidates.
if (!file_exists($path)) {
$this->config = [];
return $this;
}
$this->config = (array) Yaml::parse(file_get_contents($path));
return $this;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Consolidation\Config\Util;
/**
* Useful array utilities.
*/
class ArrayUtil
{
/**
* Merges arrays recursively while preserving.
*
* @param array $array1
* @param array $array2
*
* @return array
*
* @see http://php.net/manual/en/function.array-merge-recursive.php#92195
* @see https://github.com/grasmash/bolt/blob/robo-rebase/src/Robo/Common/ArrayManipulator.php#L22
*/
public static function mergeRecursiveDistinct(
array &$array1,
array &$array2
) {
$merged = $array1;
foreach ($array2 as $key => &$value) {
$merged[$key] = self::mergeRecursiveValue($merged, $key, $value);
}
return $merged;
}
/**
* Process the value in an mergeRecursiveDistinct - make a recursive
* call if needed.
*/
protected static function mergeRecursiveValue(&$merged, $key, $value)
{
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
return self::mergeRecursiveDistinct($merged[$key], $value);
}
return $value;
}
/**
* Fills all of the leaf-node values of a nested array with the
* provided replacement value.
*/
public static function fillRecursive(array $data, $fill)
{
$result = [];
foreach ($data as $key => $value) {
$result[$key] = $fill;
if (self::isAssociative($value)) {
$result[$key] = self::fillRecursive($value, $fill);
}
}
return $result;
}
/**
* Return true if the provided parameter is an array, and at least
* one key is non-numeric.
*/
public static function isAssociative($testArray)
{
if (!is_array($testArray)) {
return false;
}
foreach (array_keys($testArray) as $key) {
if (!is_numeric($key)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Consolidation\Config\Util;
/**
* Fetch a configuration value from a configuration group. If the
* desired configuration value is not found in the most specific
* group named, keep stepping up to the next parent group until a
* value is located.
*
* Given the following constructor inputs:
* - $prefix = "command."
* - $group = "foo.bar.baz"
* - $postfix = ".options."
* Then the `get` method will then consider, in order:
* - command.foo.bar.baz.options
* - command.foo.bar.options
* - command.foo.options
* If any of these contain an option for "$key", then return its value.
*/
class ConfigFallback extends ConfigGroup
{
/**
* @inheritdoc
*/
public function get($key)
{
return $this->getWithFallback($key, $this->group, $this->prefix, $this->postfix);
}
/**
* Fetch an option value from a given key, or, if that specific key does
* not contain a value, then consult various fallback options until a
* value is found.
*
*/
protected function getWithFallback($key, $group, $prefix = '', $postfix = '.')
{
$configKey = "{$prefix}{$group}${postfix}{$key}";
if ($this->config->has($configKey)) {
return $this->config->get($configKey);
}
if ($this->config->hasDefault($configKey)) {
return $this->config->getDefault($configKey);
}
$moreGeneralGroupname = $this->moreGeneralGroupName($group);
if ($moreGeneralGroupname) {
return $this->getWithFallback($key, $moreGeneralGroupname, $prefix, $postfix);
}
return null;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Consolidation\Config\Util;
/**
* Fetch a configuration value from a configuration group. If the
* desired configuration value is not found in the most specific
* group named, keep stepping up to the next parent group until a
* value is located.
*
* Given the following constructor inputs:
* - $prefix = "command."
* - $group = "foo.bar.baz"
* - $postfix = ".options."
* Then the `get` method will then consider, in order:
* - command.foo.bar.baz.options
* - command.foo.bar.options
* - command.foo.options
* If any of these contain an option for "$key", then return its value.
*/
abstract class ConfigGroup
{
protected $config;
protected $group;
protected $prefix;
protected $postfix;
public function __construct($config, $group, $prefix = '', $postfix = '.')
{
$this->config = $config;
$this->group = $group;
$this->prefix = $prefix;
$this->postfix = $postfix;
}
/**
* Return a description of the configuration group (with prefix and postfix).
*/
public function describe($property)
{
return $this->prefix . $this->group . $this->postfix . $property;
}
/**
* Get the requested configuration key from the most specific configuration
* group that contains it.
*/
abstract public function get($key);
/**
* Given a group name, such as "foo.bar.baz", return the next configuration
* group in the fallback hierarchy, e.g. "foo.bar".
*/
protected function moreGeneralGroupName($group)
{
$result = preg_replace('#\.[^.]*$#', '', $group);
if ($result != $group) {
return $result;
}
return false;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Consolidation\Config\Util;
/**
* Works like 'getWithFallback', but merges results from all applicable
* groups. Settings from most specific group take precedence.
*/
class ConfigMerge extends ConfigGroup
{
/**
* @inheritdoc
*/
public function get($key)
{
return $this->getWithMerge($key, $this->group, $this->prefix, $this->postfix);
}
/**
* Merge available configuration from each configuration group.
*/
public function getWithMerge($key, $group, $prefix = '', $postfix = '.')
{
$configKey = "{$prefix}{$group}${postfix}{$key}";
$result = $this->config->get($configKey, []);
if (!is_array($result)) {
throw new \UnexpectedValueException($configKey . ' must be a list of settings to apply.');
}
$moreGeneralGroupname = $this->moreGeneralGroupName($group);
if ($moreGeneralGroupname) {
$result += $this->getWithMerge($key, $moreGeneralGroupname, $prefix, $postfix);
}
return $result;
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
use Consolidation\Config\ConfigInterface;
/**
* Overlay different configuration objects that implement ConfigInterface
* to make a priority-based, merged configuration object.
*
* Note that using a ConfigOverlay hides the defaults stored in each
* individual configuration context. When using overlays, always call
* getDefault / setDefault on the ConfigOverlay object itself.
*/
class ConfigOverlay implements ConfigInterface
{
protected $contexts = [];
const DEFAULT_CONTEXT = 'default';
const PROCESS_CONTEXT = 'process';
public function __construct()
{
$this->contexts[self::DEFAULT_CONTEXT] = new Config();
$this->contexts[self::PROCESS_CONTEXT] = new Config();
}
/**
* Add a named configuration object to the configuration overlay.
* Configuration objects added LAST have HIGHEST priority, with the
* exception of the fact that the process context always has the
* highest priority.
*
* If a context has already been added, its priority will not change.
*/
public function addContext($name, ConfigInterface $config)
{
$process = $this->contexts[self::PROCESS_CONTEXT];
unset($this->contexts[self::PROCESS_CONTEXT]);
$this->contexts[$name] = $config;
$this->contexts[self::PROCESS_CONTEXT] = $process;
return $this;
}
/**
* Add a placeholder context that will be prioritized higher than
* existing contexts. This is done to ensure that contexts added
* later will maintain a higher priority if the placeholder context
* is later relaced with a different configuration set via addContext().
*
* @param string $name
* @return $this
*/
public function addPlaceholder($name)
{
return $this->addContext($name, new Config());
}
/**
* Increase the priority of the named context such that it is higher
* in priority than any existing context except for the 'process'
* context.
*
* @param string $name
* @return $this
*/
public function increasePriority($name)
{
$config = $this->getContext($name);
unset($this->contexts[$name]);
return $this->addContext($name, $config);
}
public function hasContext($name)
{
return isset($this->contexts[$name]);
}
public function getContext($name)
{
if ($this->hasContext($name)) {
return $this->contexts[$name];
}
return new Config();
}
public function removeContext($name)
{
unset($this->contexts[$name]);
}
/**
* Determine if a non-default config value exists.
*/
public function findContext($key)
{
foreach (array_reverse($this->contexts) as $name => $config) {
if ($config->has($key)) {
return $config;
}
}
return false;
}
/**
* @inheritdoc
*/
public function has($key)
{
return $this->findContext($key) != false;
}
/**
* @inheritdoc
*/
public function get($key, $default = null)
{
$context = $this->findContext($key);
if ($context) {
return $context->get($key, $default);
}
return $default;
}
/**
* @inheritdoc
*/
public function set($key, $value)
{
$this->contexts[self::PROCESS_CONTEXT]->set($key, $value);
return $this;
}
/**
* @inheritdoc
*/
public function import($data)
{
$this->unsupported(__FUNCTION__);
}
/**
* @inheritdoc
*/
public function replace($data)
{
$this->unsupported(__FUNCTION__);
}
/**
* @inheritdoc
*/
public function combine($data)
{
$this->unsupported(__FUNCTION__);
}
/**
* @inheritdoc
*/
protected function unsupported($fn)
{
throw new \Exception("The method '$fn' is not supported for the ConfigOverlay class.");
}
/**
* @inheritdoc
*/
public function export()
{
$export = [];
foreach ($this->contexts as $name => $config) {
$export = array_merge_recursive($export, $config->export());
}
return $export;
}
/**
* @inheritdoc
*/
public function hasDefault($key)
{
return $this->contexts[self::DEFAULT_CONTEXT]->has($key);
}
/**
* @inheritdoc
*/
public function getDefault($key, $default = null)
{
return $this->contexts[self::DEFAULT_CONTEXT]->get($key, $default);
}
/**
* @inheritdoc
*/
public function setDefault($key, $value)
{
$this->contexts[self::DEFAULT_CONTEXT]->set($key, $value);
return $this;
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
use Consolidation\Config\ConfigInterface;
/**
* Provide a configuration object that fetches values from environment
* variables.
*/
class EnvConfig implements ConfigInterface
{
/** @var string */
protected $prefix;
/**
* EnvConfig constructor
*
* @param $prefix The string to appear before every environment
* variable key. For example, if the prefix is 'MYAPP_', then
* the key 'foo.bar' will be fetched from the environment variable
* MYAPP_FOO_BAR.
*/
public function __construct($prefix)
{
// Ensure that the prefix is always uppercase, and always
// ends with a '_', regardless of the form the caller provided.
$this->prefix = strtoupper(rtrim($prefix, '_')) . '_';
}
/**
* @inheritdoc
*/
public function has($key)
{
return $this->get($key) !== null;
}
/**
* @inheritdoc
*/
public function get($key, $defaultFallback = null)
{
$envKey = $this->prefix . strtoupper(strtr($key, '.-', '__'));
$envKey = str_replace($this->prefix . $this->prefix, $this->prefix, $envKey);
return getenv($envKey) ?: $defaultFallback;
}
/**
* @inheritdoc
*/
public function set($key, $value)
{
throw new \Exception('Cannot call "set" on environmental configuration.');
}
/**
* @inheritdoc
*/
public function import($data)
{
// no-op
}
/**
* @inheritdoc
*/
public function export()
{
return [];
}
/**
* @inheritdoc
*/
public function hasDefault($key)
{
return false;
}
/**
* @inheritdoc
*/
public function getDefault($key, $defaultFallback = null)
{
return $defaultFallback;
}
/**
* @inheritdoc
*/
public function setDefault($key, $value)
{
throw new \Exception('Cannot call "setDefault" on environmental configuration.');
}
}

View File

@@ -1 +0,0 @@
../../tests

View File

@@ -0,0 +1,130 @@
<?php
namespace Consolidation\Config\Inject;
use Consolidation\Config\Config;
use Consolidation\TestUtils\MyFooCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;
class ConfigForCommandTest extends \PHPUnit_Framework_TestCase
{
protected $config;
protected function setUp()
{
$data = [
// Global options
'options' => [
'global' => 'from-config',
],
// Define some configuration settings for the options for
// the commands my:foo and my:bar.
'command' => [
'my' => [
// commands.my.options.* apply to all my:* commands.
'options' => [
'dir' => '/etc/common',
'priority' => 'normal',
],
'foo' => [
// commands.my.foo.options.* apply only to the my:foo command.
'options' => [
'name' => 'baz',
],
],
],
],
];
$this->config = new Config($data);
}
public function testInjection()
{
$command = new MyFooCommand();
$input = new StringInput('my:foo');
list($status, $output) = $this->runCommandViaApplication($command, $input);
$expectedOutput = <<< EOT
Enter my:foo
dir: /etc/common
name: baz
other: fish
EOT;
$this->assertEquals(0, $status);
$this->assertEquals($expectedOutput, $output);
}
public function testInjectionWithOverride()
{
$command = new MyFooCommand();
$input = new StringInput('my:foo --name=Fred');
list($status, $output) = $this->runCommandViaApplication($command, $input);
$expectedOutput = <<< EOT
Enter my:foo
dir: /etc/common
name: Fred
other: fish
EOT;
$this->assertEquals(0, $status);
$this->assertEquals($expectedOutput, $output);
}
public function testHelpDefaultInjection()
{
$command = new MyFooCommand();
$input = new StringInput('help my:foo');
list($status, $output) = $this->runCommandViaApplication($command, $input);
$expectedOutput = <<< EOT
What is the name of the thing we are naming [default: "baz"]
EOT;
$this->assertEquals(0, $status);
$this->assertContains($expectedOutput, $output);
$expectedOutput = <<< EOT
A certain global option. [default: "from-config"]
EOT;
$this->assertContains($expectedOutput, $output);
}
protected function runCommandViaApplication($command, $input)
{
$application = new Application('TestApplication', '0.0.0');
$application->getDefinition()
->addOption(
new InputOption('--global', null, InputOption::VALUE_REQUIRED, 'A certain global option.', 'hardcoded')
);
$output = new BufferedOutput();
$configInjector = new ConfigForCommand($this->config);
$configInjector->setApplication($application);
$eventDispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher();
$eventDispatcher->addSubscriber($configInjector);
$application->setDispatcher($eventDispatcher);
$application->setAutoExit(false);
$application->add($command);
$statusCode = $application->run($input, $output);
$commandOutput = trim($output->fetch());
return [$statusCode, $commandOutput];
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Consolidation\Config\Inject;
use Consolidation\Config\Config;
use Consolidation\TestUtils\ApplyConfigTestTarget;
class ConfigForSettersTest extends \PHPUnit_Framework_TestCase
{
public function testApplyConfig()
{
$data = [
// Define some configuration settings for the configuration
// of some task \My\Tasks\Operations\Frobulate.
'task' => [
'Operations' => [
// task.Operations.settings apply to all tasks in
// any *.Tass.Operations namespace.
'settings' => [
'dir' => '/base/dir',
],
'Frobulate' => [
// task.Operations.Frobulate.settings applies only
// the Frobulate task.
'settings' => [
'dir' => '/override/dir',
],
],
],
],
];
$config = new Config($data);
$applicator = new ConfigForSetters($config, 'Operations.Frobulate', 'task.');
$testTarget = new ApplyConfigTestTarget();
$applicator->apply($testTarget, 'settings');
$this->assertEquals('/override/dir', $testTarget->getDir());
$this->assertEquals(null, $testTarget->getBad());
}
public function testApplyBadConfig()
{
$data = [
// Define some configuration settings for the configuration
// of some task \My\Tasks\Operations\Frobulate.
'task' => [
'Operations' => [
// task.Operations.settings apply to all tasks in
// any *.Tass.Operations namespace.
'settings' => [
'dir' => '/base/dir',
],
'Frobulate' => [
// task.Operations.Frobulate.settings applies only
// the Frobulate task.
'settings' => [
'bad' => 'fire truck',
],
],
],
],
];
$config = new Config($data);
$applicator = new ConfigForSetters($config, 'Operations.Frobulate', 'task.');
$testTarget = new ApplyConfigTestTarget();
$exceptionMessage = '';
try
{
$applicator->apply($testTarget, 'settings');
}
catch (\Exception $e)
{
$exceptionMessage = $e->getMessage();
}
// We would prefer it if bad methods were never called; unfortunately,
// declaring the return type of a method cannot be done in a reliable
// way (via reflection) until php 7, so we allow these methods to be
// called for now.
$this->assertEquals('fire truck', $testTarget->getBad());
$this->assertEquals('Consolidation\\TestUtils\\ApplyConfigTestTarget::bad did not return \'$this\' when processing task.Operations.Frobulate.settings.', $exceptionMessage);
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
class ConfigGroupTest extends \PHPUnit_Framework_TestCase
{
protected $config;
protected function setUp()
{
$data = [
// Define some configuration settings for the options for
// the commands my:foo and my:bar.
'command' => [
'my' => [
// commands.my.options.* apply to all my:* commands.
'options' => [
'path' => '/etc/common',
'priority' => 'normal',
],
'foo' => [
// commands.my.foo.options.* apply only to the my:foo command.
'options' => [
'name' => 'baz',
],
],
'bar' => [
// Similarly, commands.my.bar.options is for the my:bar command.
'options' => [
'priority' => 'high',
],
],
],
],
// Define some configuration settings for the configuration
// of some task \My\Tasks\Operations\Frobulate.
'task' => [
'Operations' => [
// task.Operations.settings apply to all tasks in
// any *.Tass.Operations namespace.
'settings' => [
'dir' => '/base/dir',
],
'Frobulate' => [
// task.Operations.Frobulate.settings applies only
// the Frobulate task.
'settings' => [
'object' => 'fire truck',
],
],
],
],
];
$this->config = new Config($data);
}
public function testDotNotation()
{
// Test the test
$this->assertEquals('baz', $this->config->get('command.my.foo.options.name'));
}
public function testFallback()
{
$fooFallback = new ConfigFallback($this->config, 'my.foo', 'command.', '.options.');
$barFallback = new ConfigFallback($this->config, 'my.bar', 'command.', '.options.');
$this->assertEquals(null, $barFallback->get('name'));
$this->assertEquals('baz', $fooFallback->get('name'));
$this->assertEquals('high', $barFallback->get('priority'));
$this->assertEquals('normal', $fooFallback->get('priority'));
$this->assertEquals('/etc/common', $barFallback->get('path'));
$this->assertEquals('/etc/common', $fooFallback->get('path'));
}
public function testMerge()
{
$frobulateMerge = new ConfigMerge($this->config, 'Operations.Frobulate', 'task.');
$settings = $frobulateMerge->get('settings');
$this->assertEquals('fire truck', $settings['object']);
$this->assertEquals('/base/dir', $settings['dir']);
$keys = array_keys($settings);
sort($keys);
$this->assertEquals('dir,object', implode(',', $keys));
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Consolidation\Config\Loader;
class ConfigLoaderTest extends \PHPUnit_Framework_TestCase
{
public function testConfigLoader()
{
$loader = new YamlConfigLoader();
// Assert that our test data exists (test the test)
$path = __DIR__ . '/data/config-1.yml';
$this->assertTrue(file_exists($path));
$loader->load($path);
$configFile = basename($loader->getSourceName());
$this->assertEquals('config-1.yml', $configFile);
// Make sure that the data we loaded contained the expected keys
$keys = $loader->keys();
sort($keys);
$keysString = implode(',', $keys);
$this->assertEquals('c,m', $keysString);
$configData = $loader->export();
$this->assertEquals('foo', $configData['c']);
$this->assertEquals('1', $configData['m'][0]);
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
use Consolidation\Config\Loader\ConfigProcessor;
use Consolidation\Config\Loader\YamlConfigLoader;
class ConfigOverlayTest extends \PHPUnit_Framework_TestCase
{
protected $overlay;
protected function setUp()
{
$aliasContext = new Config();
$aliasContext->import([
'hidden-by-a' => 'alias hidden-by-a',
'hidden-by-process' => 'alias hidden-by-process',
'options' =>[
'a-a' => 'alias-a',
],
'command' => [
'foo' => [
'bar' => [
'command' => [
'options' => [
'a-b' => 'alias-b',
],
],
],
],
],
]);
$configFileContext = new Config();
$configFileContext->import([
'hidden-by-cf' => 'config-file hidden-by-cf',
'hidden-by-a' => 'config-file hidden-by-a',
'hidden-by-process' => 'config-file hidden-by-process',
'options' =>[
'cf-a' => 'config-file-a',
],
'command' => [
'foo' => [
'bar' => [
'command' => [
'options' => [
'cf-b' => 'config-file-b',
],
],
],
],
],
]);
$this->overlay = new ConfigOverlay();
$this->overlay->set('hidden-by-process', 'process-h');
$this->overlay->addContext('cf', $configFileContext);
$this->overlay->addContext('a', $aliasContext);
$this->overlay->setDefault('df-a', 'default');
$this->overlay->setDefault('hidden-by-a', 'default hidden-by-a');
$this->overlay->setDefault('hidden-by-cf', 'default hidden-by-cf');
$this->overlay->setDefault('hidden-by-process', 'default hidden-by-process');
}
public function testGetPriority()
{
$this->assertEquals('process-h', $this->overlay->get('hidden-by-process'));
$this->assertEquals('config-file hidden-by-cf', $this->overlay->get('hidden-by-cf'));
$this->assertEquals('alias hidden-by-a', $this->overlay->get('hidden-by-a'));
}
public function testDefault()
{
$this->assertEquals('alias-a', $this->overlay->get('options.a-a'));
$this->assertEquals('alias-a', $this->overlay->get('options.a-a', 'ignored'));
$this->assertEquals('default', $this->overlay->getDefault('df-a', 'ignored'));
$this->assertEquals('nsv', $this->overlay->getDefault('a-a', 'nsv'));
$this->overlay->setDefault('df-a', 'new value');
$this->assertEquals('new value', $this->overlay->getDefault('df-a', 'ignored'));
}
public function testExport()
{
$data = $this->overlay->export();
$this->assertEquals('config-file-a', $data['options']['cf-a']);
$this->assertEquals('alias-a', $data['options']['a-a']);
}
/**
* @expectedException Exception
*/
public function testImport()
{
$data = $this->overlay->import(['a' => 'value']);
}
public function testMaintainPriority()
{
// Get and re-add the 'cf' context. Its priority should not change.
$configFileContext = $this->overlay->getContext('cf');
$this->overlay->addContext('cf', $configFileContext);
// These asserts are the same as in testGetPriority
$this->assertEquals('process-h', $this->overlay->get('hidden-by-process'));
$this->assertEquals('config-file hidden-by-cf', $this->overlay->get('hidden-by-cf'));
$this->assertEquals('alias hidden-by-a', $this->overlay->get('hidden-by-a'));
}
public function testChangePriority()
{
// Increase the priority of the 'cf' context. Now, it should have a higher
// priority than the 'alias' context, but should still have a lower
// priority than the 'process' context.
$this->overlay->increasePriority('cf');
// These asserts are the same as in testGetPriority
$this->assertEquals('process-h', $this->overlay->get('hidden-by-process'));
$this->assertEquals('config-file hidden-by-cf', $this->overlay->get('hidden-by-cf'));
// This one has changed: the config-file value is now found instead
// of the alias value.
$this->assertEquals('config-file hidden-by-a', $this->overlay->get('hidden-by-a'));
}
public function testPlaceholder()
{
$this->overlay->addPlaceholder('lower');
$higherContext = new Config();
$higherContext->import(['priority-test' => 'higher']);
$lowerContext = new Config();
$lowerContext->import(['priority-test' => 'lower']);
// Usually 'lower' would have the highest priority, since it is
// added last. However, our earlier call to 'addPlaceholder' reserves
// a spot for it, so the 'higher' context will end up with a higher
// priority.
$this->overlay->addContext('higher', $higherContext);
$this->overlay->addContext('lower', $lowerContext);
$this->assertEquals('higher', $this->overlay->get('priority-test', 'neither'));
// Test to see that we can change the value of the 'higher' context,
// and the change will be reflected in the overlay.
$higherContext->set('priority-test', 'changed');
$this->assertEquals('changed', $this->overlay->get('priority-test', 'neither'));
// Test to see that the 'process' context still has the highest priority.
$this->overlay->set('priority-test', 'process');
$higherContext->set('priority-test', 'ignored');
$this->assertEquals('process', $this->overlay->get('priority-test', 'neither'));
}
public function testDoesNotHave()
{
$context = $this->overlay->getContext('no-such-context');
$data = $context->export();
$this->assertEquals('[]', json_encode($data));
$this->assertTrue(!$this->overlay->has('no-such-key'));
$this->assertTrue(!$this->overlay->hasDefault('no-such-default'));
$this->assertEquals('no-such-value', $this->overlay->get('no-such-key', 'no-such-value'));
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace Consolidation\Config\Loader;
use Consolidation\TestUtils\TestLoader;
class ConfigProcessorTest extends \PHPUnit_Framework_TestCase
{
public function testConfigProcessorAdd()
{
$config1 = [
'c' => 'foo',
'm' => [1],
];
$config2 = [
'b' => '${c}bar',
'm' => [2],
];
$config3 = [
'a' => '${b}baz',
'm' => [3],
];
$processor = new ConfigProcessor();
$processor->add($config1);
$processor->add($config2);
$processor->add($config3);
$data = $processor->export();
$this->assertEquals('foo', $data['c']);
$this->assertEquals('foobar', $data['b']);
$this->assertEquals('foobarbaz', $data['a']);
}
public function processorForConfigMergeTest($provideSourceNames)
{
$config1 = [
'm' => [
'x' => 'x-1',
'y' => [
'r' => 'r-1',
's' => 's-1',
't' => 't-1',
],
'z' => 'z-1',
],
];
$config2 = [
'm' => [
'w' => 'w-2',
'y' => [
'q' => 'q-2',
's' => 's-2',
],
'z' => 'z-2',
],
];
$config3 = [
'm' => [
'v' => 'v-3',
'y' => [
't' => 't-3',
'u' => 'u-3',
],
'z' => 'z-3',
],
];
$processor = new ConfigProcessor();
$testLoader = new TestLoader();
$testLoader->set($config1);
$testLoader->setSourceName($provideSourceNames ? 'c-1' : '');
$processor->extend($testLoader);
$testLoader->set($config2);
$testLoader->setSourceName($provideSourceNames ? 'c-2' : '');
$processor->extend($testLoader);
$testLoader->set($config3);
$testLoader->setSourceName($provideSourceNames ? 'c-3' : '');
$processor->extend($testLoader);
return $processor;
}
public function testConfigProcessorMergeAssociative()
{
$processor = $this->processorForConfigMergeTest(false);
$data = $processor->export();
$this->assertEquals('{"m":{"x":"x-1","y":{"r":"r-1","s":"s-2","t":"t-3","q":"q-2","u":"u-3"},"z":"z-3","w":"w-2","v":"v-3"}}', json_encode($data));
}
public function testConfigProcessorMergeAssociativeWithSourceNames()
{
$processor = $this->processorForConfigMergeTest(true);
$sources = $processor->sources();
$data = $processor->export();
$this->assertEquals('{"m":{"x":"x-1","y":{"r":"r-1","s":"s-2","t":"t-3","q":"q-2","u":"u-3"},"z":"z-3","w":"w-2","v":"v-3"}}', json_encode($data));
$this->assertEquals('c-1', $sources['m']['x']);
$this->assertEquals('c-1', $sources['m']['y']['r']);
$this->assertEquals('c-2', $sources['m']['w']);
$this->assertEquals('c-2', $sources['m']['y']['s']);
$this->assertEquals('c-3', $sources['m']['z']);
$this->assertEquals('c-3', $sources['m']['y']['u']);
}
public function testConfiProcessorSources()
{
$processor = new ConfigProcessor();
$loader = new YamlConfigLoader();
$processor->extend($loader->load(__DIR__ . '/data/config-1.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-2.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-3.yml'));
$sources = $processor->sources();
$data = $processor->export();
$this->assertEquals('foo', $data['c']);
$this->assertEquals('foobar', $data['b']);
$this->assertEquals('foobarbaz', $data['a']);
$this->assertEquals('3', $data['m'][0]);
$this->assertEquals( __DIR__ . '/data/config-3.yml', $sources['a']);
$this->assertEquals( __DIR__ . '/data/config-2.yml', $sources['b']);
$this->assertEquals( __DIR__ . '/data/config-1.yml', $sources['c']);
$this->assertEquals( __DIR__ . '/data/config-3.yml', $sources['m']);
}
public function testConfiProcessorSourcesLoadInReverseOrder()
{
$processor = new ConfigProcessor();
$loader = new YamlConfigLoader();
$processor->extend($loader->load(__DIR__ . '/data/config-3.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-2.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-1.yml'));
$sources = $processor->sources();
$data = $processor->export();
$this->assertEquals('foo', $data['c']);
$this->assertEquals('foobar', $data['b']);
$this->assertEquals('foobarbaz', $data['a']);
$this->assertEquals('1', $data['m'][0]);
$this->assertEquals( __DIR__ . '/data/config-3.yml', $sources['a']);
$this->assertEquals( __DIR__ . '/data/config-2.yml', $sources['b']);
$this->assertEquals( __DIR__ . '/data/config-1.yml', $sources['c']);
$this->assertEquals( __DIR__ . '/data/config-1.yml', $sources['m']);
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Consolidation\Config;
use Consolidation\Config\Loader\ConfigProcessor;
use Consolidation\Config\Loader\YamlConfigLoader;
class ConfigTest extends \PHPUnit_Framework_TestCase
{
public function testSetters()
{
// Pointless tests just to ensure everything is covered.
$config = new Config();
$config->set('foo', 'bar');
$data = $config->export();
$this->assertEquals('{"foo":"bar"}', json_encode($data));
}
public function testCombine()
{
// Pointless tests just to ensure everything is covered.
$config = new Config();
$config->set('foo', 'bar');
$config->set('baz', 'boz');
$config2 = new Config();
$config2->set('foo', 'fu');
$config2->set('new', 'blue');
$config->combine($config2->export());
$this->assertEquals('fu', $config->get('foo'));
$this->assertEquals('boz', $config->get('baz'));
$this->assertEquals('blue', $config->get('new'));
}
public function testDefault()
{
$data = [
'a' => 'foo',
'b' => 'bar',
'c' => 'boz',
];
$foo = ["foo" => "bar"];
$config = new Config($data);
$config->setDefault('c', 'other');
$config->setDefault('d', 'other');
$config->setDefault('f', $foo);
$this->assertEquals('foo', $config->get('a'));
$this->assertEquals('boz', $config->get('c'));
$this->assertEquals('other', $config->get('d'));
$this->assertEquals('other', $config->getDefault('c'));
$this->assertEquals('', $config->get('e'));
$this->assertEquals('bar', $config->get('f.foo'));
$this->assertEquals('{"foo":"bar"}', json_encode($config->get('f')));
}
public function testDefaultsArray()
{
$data = ['a' => 'foo', 'b' => 'bar', 'c' => 'boz',];
$defaults = ['d' => 'foo', 'e' => 'bar', 'f' => 'boz',];
// Create reflection class to test private methods
$configClass = new \ReflectionClass("Consolidation\Config\Config");
// $defaults
$defaultsProperty = $configClass->getProperty("defaults");
$defaultsProperty->setAccessible(true);
// $getDefaults
$getDefaultsMethod = $configClass->getMethod("getDefaults");
$getDefaultsMethod->setAccessible(true);
// Test the config class
$config = new Config($data);
// Set $config::defaults to an array to test getter and setter
$defaultsProperty->setValue($config, $defaults);
$this->assertTrue(is_array($defaultsProperty->getValue($config)));
$this->assertInstanceOf('Dflydev\DotAccessData\Data',
$getDefaultsMethod->invoke($config));
// Set $config::defaults to a string to test exception
$defaultsProperty->setValue($config, "foo.bar");
$this->setExpectedException("Exception");
$getDefaultsMethod->invoke($config);
}
public function testConfigurationWithCrossFileReferences()
{
$config = new Config();
$processor = new ConfigProcessor();
$loader = new YamlConfigLoader();
$processor->extend($loader->load(__DIR__ . '/data/config-1.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-2.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-3.yml'));
// Does not fail if configuration file cannot be found
$processor->extend($loader->load(__DIR__ . '/data/no-such-file.yml'));
// We must capture the sources before exporting, as export
// dumps this information.
$sources = $processor->sources();
$config->import($processor->export());
$this->assertEquals(implode(',', $config->get('m')), '3');
$this->assertEquals($config->get('a'), 'foobarbaz');
$this->assertEquals($sources['a'], __DIR__ . '/data/config-3.yml');
$this->assertEquals($sources['b'], __DIR__ . '/data/config-2.yml');
$this->assertEquals($sources['c'], __DIR__ . '/data/config-1.yml');
}
public function testConfigurationWithReverseOrderCrossFileReferences()
{
$config = new Config();
$processor = new ConfigProcessor();
$loader = new YamlConfigLoader();
$processor->extend($loader->load(__DIR__ . '/data/config-3.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-2.yml'));
$processor->extend($loader->load(__DIR__ . '/data/config-1.yml'));
$sources = $processor->sources();
$config->import($processor->export());
$this->assertEquals(implode(',', $config->get('m')), '1');
if (strpos($config->get('a'), '$') !== false) {
throw new \PHPUnit_Framework_SkippedTestError(
'Evaluation of cross-file references in reverse order not supported.'
);
}
$this->assertEquals($config->get('a'), 'foobarbaz');
$this->assertEquals($sources['a'], __DIR__ . '/data/config-3.yml');
$this->assertEquals($sources['b'], __DIR__ . '/data/config-2.yml');
$this->assertEquals($sources['c'], __DIR__ . '/data/config-1.yml');
}
}

View File

@@ -0,0 +1,3 @@
c: foo
m:
- 1

View File

@@ -0,0 +1,3 @@
b: ${c}bar
m:
- 2

View File

@@ -0,0 +1,3 @@
a: ${b}baz
m:
- 3

View File

@@ -0,0 +1,23 @@
#!/bin/bash
SCENARIO=$1
ACTION=${2-install}
dir=dependencies/${SCENARIO}
if [ -z "$SCENARIO" ] ; then
SCENARIO=default
dir=.
fi
if [ ! -d "$dir" ] ; then
echo "Requested scenario '${SCENARIO}' does not exist."
exit 1
fi
echo "Switch to ${SCENARIO} scenario"
set -ex
composer -n --working-dir=$dir ${ACTION} --prefer-dist --no-scripts
composer -n --working-dir=$dir info

View File

@@ -0,0 +1,66 @@
#!/bin/bash
#
# This script is called automatically on every `composer update`.
# See "post-update-cmd" in the "scripts" section of composer.json.
#
# This script will create a derived composer.json / composer.lock
# pair for every test scenario. Test scenarios are defined in the
# "scenarios" file, which should be customized to suit the needs
# of the project.
#
SELF_DIRNAME="`dirname -- "$0"`"
source ${SELF_DIRNAME}/scenarios
echo
echo "::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"
echo "::"
echo ":: Update dependencies for the following scenarios:"
echo "::"
echo ":: ${SCENARIOS}"
echo "::"
echo "::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"
echo
set -ex
for SCENARIO in ${SCENARIOS} ; do
dir=dependencies/${SCENARIO}
# Define indirect variable names
stability_variable="stability_${SCENARIO}"
requirement_variable="requirement_${SCENARIO}"
platform_php_variable="platform_php_${SCENARIO}"
echo "### Create $dir/composer.json for ${SCENARIO} scenario"
mkdir -p $dir
cp composer.json $dir
# Then set our own platform php version if applicable (otherwise unset it)
composer -n --working-dir=$dir config platform.php "${!platform_php_variable---unset}"
# Temporarily set our vendor directory to 'vendor'
composer -n --working-dir=$dir config vendor-dir vendor
# Set an appropriate minimum stability for this version of Symfony
composer -n --working-dir=$dir config minimum-stability "${!stability_variable-stable}"
# Add a constraint to limit the Symfony version
composer -n --working-dir=$dir require --dev --no-update "${!requirement_variable}"
# Create the composer.lock file. Ignore the vendor directory created.
composer -n --working-dir=$dir update --no-scripts
# Set the vendor directory to its final desired location.
composer -n --working-dir=$dir config vendor-dir '../../vendor'
# The 'autoload' section specifies directory paths that are relative
# to the composer.json file. We will drop in some symlinks so that
# these paths will resolve as if the composer.json were in the root.
for target in $AUTOLOAD_DIRECTORIES ; do
ln -s -f ../../$target $dir
done
done

View File

@@ -0,0 +1,12 @@
#!/bin/bash
SCENARIOS="symfony2 symfony3 symfony4"
AUTOLOAD_DIRECTORIES='src tests'
platform_php_symfony2='5.4'
platform_php_symfony3='5.6'
requirement_symfony2='symfony/console:^2.8'
requirement_symfony3='symfony/console:^3'
requirement_symfony4='symfony/console:^4'

View File

@@ -0,0 +1,43 @@
<?php
namespace Consolidation\TestUtils;
class ApplyConfigTestTarget
{
protected $dir;
protected $value;
/**
* A proper setter for the 'dir' property
*/
public function dir($dir)
{
$this->dir = $dir;
return $this;
}
/**
* A getter for the 'dir' property that we will use to
* determine if the setter was called.
*/
public function getDir()
{
return $this->dir;
}
/**
* A bad setter that does not return $this.
*/
public function bad($value)
{
$this->value = $value;
}
/**
* A getter for the bad setter.
*/
public function getBad()
{
return $this->value;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Consolidation\TestUtils;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
class MyFooCommand extends Command
{
protected function configure()
{
$this
->setName('my:foo')
->setDescription('My foo command.')
->setHelp('This command tests command option injection by echoing its options')
->addOption(
'other',
null,
InputOption::VALUE_REQUIRED,
'Some other option',
'fish'
)
->addOption(
'name',
null,
InputOption::VALUE_REQUIRED,
'What is the name of the thing we are naming',
'George'
)
->addOption(
'dir',
null,
InputOption::VALUE_REQUIRED,
'What is the base directory to use for this command',
'/default/path'
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln('Enter my:foo');
$output->writeln('dir: ' . $input->getOption('dir'));
$output->writeln('name: ' . $input->getOption('name'));
$output->writeln('other: ' . $input->getOption('other'));
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Consolidation\TestUtils;
use Consolidation\Config\Loader\ConfigLoaderInterface;
class TestLoader implements ConfigLoaderInterface
{
protected $data;
protected $sourceName;
public function set($data)
{
$this->data = $data;
}
public function setSourceName($name)
{
$this->sourceName = $name;
}
public function export()
{
return $this->data;
}
public function keys()
{
return array_keys($this->data);
}
public function getSourceName()
{
return $this->sourceName;
}
}

View File

@@ -1 +0,0 @@
../../src

View File

@@ -0,0 +1,73 @@
<?php
namespace Robo;
use Symfony\Component\Console\Application as SymfonyApplication;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputOption;
class Application extends SymfonyApplication
{
/**
* @param string $name
* @param string $version
*/
public function __construct($name, $version)
{
parent::__construct($name, $version);
$this->getDefinition()
->addOption(
new InputOption('--simulate', null, InputOption::VALUE_NONE, 'Run in simulated mode (show what would have happened).')
);
$this->getDefinition()
->addOption(
new InputOption('--progress-delay', null, InputOption::VALUE_REQUIRED, 'Number of seconds before progress bar is displayed in long-running task collections. Default: 2s.', Config::DEFAULT_PROGRESS_DELAY)
);
$this->getDefinition()
->addOption(
new InputOption('--define', '-D', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Define a configuration item value.', [])
);
}
/**
* @param string $roboFile
* @param string $roboClass
*/
public function addInitRoboFileCommand($roboFile, $roboClass)
{
$createRoboFile = new Command('init');
$createRoboFile->setDescription("Intitalizes basic RoboFile in current dir");
$createRoboFile->setCode(function () use ($roboClass, $roboFile) {
$output = Robo::output();
$output->writeln("<comment> ~~~ Welcome to Robo! ~~~~ </comment>");
$output->writeln("<comment> ". basename($roboFile) ." will be created in the current directory </comment>");
file_put_contents(
$roboFile,
'<?php'
. "\n/**"
. "\n * This is project's console commands configuration for Robo task runner."
. "\n *"
. "\n * @see http://robo.li/"
. "\n */"
. "\nclass " . $roboClass . " extends \\Robo\\Tasks\n{\n // define public methods as commands\n}"
);
$output->writeln("<comment> Edit this file to add your commands! </comment>");
});
$this->add($createRoboFile);
}
/**
* Add self update command, do nothing if null is provided
*
* @param string $repository GitHub Repository for self update
*/
public function addSelfUpdateCommand($repository = null)
{
if (!$repository) {
return;
}
$selfUpdateCommand = new SelfUpdateCommand($this->getName(), $this->getVersion(), $repository);
$this->add($selfUpdateCommand);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Robo\Collection;
use Robo\Result;
use Robo\Contract\TaskInterface;
use Robo\State\StateAwareInterface;
use Robo\State\Data;
/**
* Creates a task wrapper that converts any Callable into an
* object that can be used directly with a task collection.
*
* It is not necessary to use this class directly; Collection will
* automatically wrap Callables when they are added.
*/
class CallableTask implements TaskInterface
{
/**
* @var callable
*/
protected $fn;
/**
* @var \Robo\Contract\TaskInterface
*/
protected $reference;
public function __construct(callable $fn, TaskInterface $reference)
{
$this->fn = $fn;
$this->reference = $reference;
}
/**
* @return \Robo\Result
*/
public function run()
{
$result = call_user_func($this->fn, $this->getState());
// If the function returns no result, then count it
// as a success.
if (!isset($result)) {
$result = Result::success($this->reference);
}
// If the function returns a result, it must either return
// a \Robo\Result or an exit code. In the later case, we
// convert it to a \Robo\Result.
if (!$result instanceof Result) {
$result = new Result($this->reference, $result);
}
return $result;
}
public function getState()
{
if ($this->reference instanceof StateAwareInterface) {
return $this->reference->getState();
}
return new Data();
}
}

View File

@@ -0,0 +1,769 @@
<?php
namespace Robo\Collection;
use Robo\Result;
use Robo\State\Data;
use Psr\Log\LogLevel;
use Robo\Contract\TaskInterface;
use Robo\Task\StackBasedTask;
use Robo\Task\BaseTask;
use Robo\TaskInfo;
use Robo\Contract\WrappedTaskInterface;
use Robo\Exception\TaskException;
use Robo\Exception\TaskExitException;
use Robo\Contract\CommandInterface;
use Robo\Contract\InflectionInterface;
use Robo\State\StateAwareInterface;
use Robo\State\StateAwareTrait;
/**
* Group tasks into a collection that run together. Supports
* rollback operations for handling error conditions.
*
* This is an internal class. Clients should use a CollectionBuilder
* rather than direct use of the Collection class. @see CollectionBuilder.
*
* Below, the example FilesystemStack task is added to a collection,
* and associated with a rollback task. If any of the operations in
* the FilesystemStack, or if any of the other tasks also added to
* the task collection should fail, then the rollback function is
* called. Here, taskDeleteDir is used to remove partial results
* of an unfinished task.
*/
class Collection extends BaseTask implements CollectionInterface, CommandInterface, StateAwareInterface
{
use StateAwareTrait;
/**
* @var \Robo\Collection\Element[]
*/
protected $taskList = [];
/**
* @var TaskInterface[]
*/
protected $rollbackStack = [];
/**
* @var TaskInterface[]
*/
protected $completionStack = [];
/**
* @var CollectionInterface
*/
protected $parentCollection;
/**
* @var callable[]
*/
protected $deferredCallbacks = [];
/**
* @var string[]
*/
protected $messageStoreKeys = [];
/**
* Constructor.
*/
public function __construct()
{
$this->resetState();
}
public function setProgressBarAutoDisplayInterval($interval)
{
if (!$this->progressIndicator) {
return;
}
return $this->progressIndicator->setProgressBarAutoDisplayInterval($interval);
}
/**
* {@inheritdoc}
*/
public function add(TaskInterface $task, $name = self::UNNAMEDTASK)
{
$task = new CompletionWrapper($this, $task);
$this->addToTaskList($name, $task);
return $this;
}
/**
* {@inheritdoc}
*/
public function addCode(callable $code, $name = self::UNNAMEDTASK)
{
return $this->add(new CallableTask($code, $this), $name);
}
/**
* {@inheritdoc}
*/
public function addIterable($iterable, callable $code)
{
$callbackTask = (new IterationTask($iterable, $code, $this))->inflect($this);
return $this->add($callbackTask);
}
/**
* {@inheritdoc}
*/
public function rollback(TaskInterface $rollbackTask)
{
// Rollback tasks always try as hard as they can, and never report failures.
$rollbackTask = $this->ignoreErrorsTaskWrapper($rollbackTask);
return $this->wrapAndRegisterRollback($rollbackTask);
}
/**
* {@inheritdoc}
*/
public function rollbackCode(callable $rollbackCode)
{
// Rollback tasks always try as hard as they can, and never report failures.
$rollbackTask = $this->ignoreErrorsCodeWrapper($rollbackCode);
return $this->wrapAndRegisterRollback($rollbackTask);
}
/**
* {@inheritdoc}
*/
public function completion(TaskInterface $completionTask)
{
$collection = $this;
$completionRegistrationTask = new CallableTask(
function () use ($collection, $completionTask) {
$collection->registerCompletion($completionTask);
},
$this
);
$this->addToTaskList(self::UNNAMEDTASK, $completionRegistrationTask);
return $this;
}
/**
* {@inheritdoc}
*/
public function completionCode(callable $completionTask)
{
$completionTask = new CallableTask($completionTask, $this);
return $this->completion($completionTask);
}
/**
* {@inheritdoc}
*/
public function before($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK)
{
return $this->addBeforeOrAfter(__FUNCTION__, $name, $task, $nameOfTaskToAdd);
}
/**
* {@inheritdoc}
*/
public function after($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK)
{
return $this->addBeforeOrAfter(__FUNCTION__, $name, $task, $nameOfTaskToAdd);
}
/**
* {@inheritdoc}
*/
public function progressMessage($text, $context = [], $level = LogLevel::NOTICE)
{
$context += ['name' => 'Progress'];
$context += TaskInfo::getTaskContext($this);
return $this->addCode(
function () use ($level, $text, $context) {
$context += $this->getState()->getData();
$this->printTaskOutput($level, $text, $context);
}
);
}
/**
* @param \Robo\Contract\TaskInterface $rollbackTask
*
* @return $this
*/
protected function wrapAndRegisterRollback(TaskInterface $rollbackTask)
{
$collection = $this;
$rollbackRegistrationTask = new CallableTask(
function () use ($collection, $rollbackTask) {
$collection->registerRollback($rollbackTask);
},
$this
);
$this->addToTaskList(self::UNNAMEDTASK, $rollbackRegistrationTask);
return $this;
}
/**
* Add either a 'before' or 'after' function or task.
*
* @param string $method
* @param string $name
* @param callable|TaskInterface $task
* @param string $nameOfTaskToAdd
*
* @return $this
*/
protected function addBeforeOrAfter($method, $name, $task, $nameOfTaskToAdd)
{
if (is_callable($task)) {
$task = new CallableTask($task, $this);
}
$existingTask = $this->namedTask($name);
$fn = [$existingTask, $method];
call_user_func($fn, $task, $nameOfTaskToAdd);
return $this;
}
/**
* Wrap the provided task in a wrapper that will ignore
* any errors or exceptions that may be produced. This
* is useful, for example, in adding optional cleanup tasks
* at the beginning of a task collection, to remove previous
* results which may or may not exist.
*
* TODO: Provide some way to specify which sort of errors
* are ignored, so that 'file not found' may be ignored,
* but 'permission denied' reported?
*
* @param \Robo\Contract\TaskInterface $task
*
* @return \Robo\Collection\CallableTask
*/
public function ignoreErrorsTaskWrapper(TaskInterface $task)
{
// If the task is a stack-based task, then tell it
// to try to run all of its operations, even if some
// of them fail.
if ($task instanceof StackBasedTask) {
$task->stopOnFail(false);
}
$ignoreErrorsInTask = function () use ($task) {
$data = [];
try {
$result = $this->runSubtask($task);
$message = $result->getMessage();
$data = $result->getData();
$data['exitcode'] = $result->getExitCode();
} catch (\Exception $e) {
$message = $e->getMessage();
}
return Result::success($task, $message, $data);
};
// Wrap our ignore errors callable in a task.
return new CallableTask($ignoreErrorsInTask, $this);
}
/**
* @param callable $task
*
* @return \Robo\Collection\CallableTask
*/
public function ignoreErrorsCodeWrapper(callable $task)
{
return $this->ignoreErrorsTaskWrapper(new CallableTask($task, $this));
}
/**
* Return the list of task names added to this collection.
*
* @return array
*/
public function taskNames()
{
return array_keys($this->taskList);
}
/**
* Test to see if a specified task name exists.
* n.b. before() and after() require that the named
* task exist; use this function to test first, if
* unsure.
*
* @param string $name
*
* @return bool
*/
public function hasTask($name)
{
return array_key_exists($name, $this->taskList);
}
/**
* Find an existing named task.
*
* @param string $name
* The name of the task to insert before. The named task MUST exist.
*
* @return Element
* The task group for the named task. Generally this is only
* used to call 'before()' and 'after()'.
*/
protected function namedTask($name)
{
if (!$this->hasTask($name)) {
throw new \RuntimeException("Could not find task named $name");
}
return $this->taskList[$name];
}
/**
* Add a list of tasks to our task collection.
*
* @param TaskInterface[] $tasks
* An array of tasks to run with rollback protection
*
* @return $this
*/
public function addTaskList(array $tasks)
{
foreach ($tasks as $name => $task) {
$this->add($task, $name);
}
return $this;
}
/**
* Add the provided task to our task list.
*
* @param string $name
* @param \Robo\Contract\TaskInterface $task
*
* @return \Robo\Collection\Collection
*/
protected function addToTaskList($name, TaskInterface $task)
{
// All tasks are stored in a task group so that we have a place
// to hang 'before' and 'after' tasks.
$taskGroup = new Element($task);
return $this->addCollectionElementToTaskList($name, $taskGroup);
}
/**
* @param int|string $name
* @param \Robo\Collection\Element $taskGroup
*
* @return $this
*/
protected function addCollectionElementToTaskList($name, Element $taskGroup)
{
// If a task name is not provided, then we'll let php pick
// the array index.
if (Result::isUnnamed($name)) {
$this->taskList[] = $taskGroup;
return $this;
}
// If we are replacing an existing task with the
// same name, ensure that our new task is added to
// the end.
$this->taskList[$name] = $taskGroup;
return $this;
}
/**
* Set the parent collection. This is necessary so that nested
* collections' rollback and completion tasks can be added to the
* top-level collection, ensuring that the rollbacks for a collection
* will run if any later task fails.
*
* @param \Robo\Collection\NestedCollectionInterface $parentCollection
*
* @return $this
*/
public function setParentCollection(NestedCollectionInterface $parentCollection)
{
$this->parentCollection = $parentCollection;
return $this;
}
/**
* Get the appropriate parent collection to use
*
* @return CollectionInterface
*/
public function getParentCollection()
{
return $this->parentCollection ? $this->parentCollection : $this;
}
/**
* Register a rollback task to run if there is any failure.
*
* Clients are free to add tasks to the rollback stack as
* desired; however, usually it is preferable to call
* Collection::rollback() instead. With that function,
* the rollback function will only be called if all of the
* tasks added before it complete successfully, AND some later
* task fails.
*
* One example of a good use-case for registering a callback
* function directly is to add a task that sends notification
* when a task fails.
*
* @param TaskInterface $rollbackTask
* The rollback task to run on failure.
*/
public function registerRollback(TaskInterface $rollbackTask)
{
if ($this->parentCollection) {
return $this->parentCollection->registerRollback($rollbackTask);
}
if ($rollbackTask) {
$this->rollbackStack[] = $rollbackTask;
}
}
/**
* Register a completion task to run once all other tasks finish.
* Completion tasks run whether or not a rollback operation was
* triggered. They do not trigger rollbacks if they fail.
*
* The typical use-case for a completion function is to clean up
* temporary objects (e.g. temporary folders). The preferred
* way to do that, though, is to use Temporary::wrap().
*
* On failures, completion tasks will run after all rollback tasks.
* If one task collection is nested inside another task collection,
* then the nested collection's completion tasks will run as soon as
* the nested task completes; they are not deferred to the end of
* the containing collection's execution.
*
* @param TaskInterface $completionTask
* The completion task to run at the end of all other operations.
*/
public function registerCompletion(TaskInterface $completionTask)
{
if ($this->parentCollection) {
return $this->parentCollection->registerCompletion($completionTask);
}
if ($completionTask) {
// Completion tasks always try as hard as they can, and never report failures.
$completionTask = $this->ignoreErrorsTaskWrapper($completionTask);
$this->completionStack[] = $completionTask;
}
}
/**
* Return the count of steps in this collection
*
* @return int
*/
public function progressIndicatorSteps()
{
$steps = 0;
foreach ($this->taskList as $name => $taskGroup) {
$steps += $taskGroup->progressIndicatorSteps();
}
return $steps;
}
/**
* A Collection of tasks can provide a command via `getCommand()`
* if it contains a single task, and that task implements CommandInterface.
*
* @return string
*
* @throws \Robo\Exception\TaskException
*/
public function getCommand()
{
if (empty($this->taskList)) {
return '';
}
if (count($this->taskList) > 1) {
// TODO: We could potentially iterate over the items in the collection
// and concatenate the result of getCommand() from each one, and fail
// only if we encounter a command that is not a CommandInterface.
throw new TaskException($this, "getCommand() does not work on arbitrary collections of tasks.");
}
$taskElement = reset($this->taskList);
$task = $taskElement->getTask();
$task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
if ($task instanceof CommandInterface) {
return $task->getCommand();
}
throw new TaskException($task, get_class($task) . " does not implement CommandInterface, so can't be used to provide a command");
}
/**
* Run our tasks, and roll back if necessary.
*
* @return \Robo\Result
*/
public function run()
{
$result = $this->runWithoutCompletion();
$this->complete();
return $result;
}
/**
* @return \Robo\Result
*/
private function runWithoutCompletion()
{
$result = Result::success($this);
if (empty($this->taskList)) {
return $result;
}
$this->startProgressIndicator();
if ($result->wasSuccessful()) {
foreach ($this->taskList as $name => $taskGroup) {
$taskList = $taskGroup->getTaskList();
$result = $this->runTaskList($name, $taskList, $result);
if (!$result->wasSuccessful()) {
$this->fail();
return $result;
}
}
$this->taskList = [];
}
$this->stopProgressIndicator();
$result['time'] = $this->getExecutionTime();
return $result;
}
/**
* Run every task in a list, but only up to the first failure.
* Return the failing result, or success if all tasks run.
*
* @param string $name
* @param TaskInterface[] $taskList
* @param \Robo\Result $result
*
* @return \Robo\Result
*
* @throws \Robo\Exception\TaskExitException
*/
private function runTaskList($name, array $taskList, Result $result)
{
try {
foreach ($taskList as $taskName => $task) {
$taskResult = $this->runSubtask($task);
$this->advanceProgressIndicator();
// If the current task returns an error code, then stop
// execution and signal a rollback.
if (!$taskResult->wasSuccessful()) {
return $taskResult;
}
// We accumulate our results into a field so that tasks that
// have a reference to the collection may examine and modify
// the incremental results, if they wish.
$key = Result::isUnnamed($taskName) ? $name : $taskName;
$result->accumulate($key, $taskResult);
// The result message will be the message of the last task executed.
$result->setMessage($taskResult->getMessage());
}
} catch (TaskExitException $exitException) {
$this->fail();
throw $exitException;
} catch (\Exception $e) {
// Tasks typically should not throw, but if one does, we will
// convert it into an error and roll back.
return Result::fromException($task, $e, $result->getData());
}
return $result;
}
/**
* Force the rollback functions to run
*
* @return $this
*/
public function fail()
{
$this->disableProgressIndicator();
$this->runRollbackTasks();
$this->complete();
return $this;
}
/**
* Force the completion functions to run
*
* @return $this
*/
public function complete()
{
$this->detatchProgressIndicator();
$this->runTaskListIgnoringFailures($this->completionStack);
$this->reset();
return $this;
}
/**
* Reset this collection, removing all tasks.
*
* @return $this
*/
public function reset()
{
$this->taskList = [];
$this->completionStack = [];
$this->rollbackStack = [];
return $this;
}
/**
* Run all of our rollback tasks.
*
* Note that Collection does not implement RollbackInterface, but
* it may still be used as a task inside another task collection
* (i.e. you can nest task collections, if desired).
*/
protected function runRollbackTasks()
{
$this->runTaskListIgnoringFailures($this->rollbackStack);
// Erase our rollback stack once we have finished rolling
// everything back. This will allow us to potentially use
// a command collection more than once (e.g. to retry a
// failed operation after doing some error recovery).
$this->rollbackStack = [];
}
/**
* @param TaskInterface|NestedCollectionInterface|WrappedTaskInterface $task
*
* @return \Robo\Result
*/
protected function runSubtask($task)
{
$original = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
$this->setParentCollectionForTask($original, $this->getParentCollection());
if ($original instanceof InflectionInterface) {
$original->inflect($this);
}
if ($original instanceof StateAwareInterface) {
$original->setState($this->getState());
}
$this->doDeferredInitialization($original);
$taskResult = $task->run();
$taskResult = Result::ensureResult($task, $taskResult);
$this->doStateUpdates($original, $taskResult);
return $taskResult;
}
protected function doStateUpdates($task, Data $taskResult)
{
$this->updateState($taskResult);
$key = spl_object_hash($task);
if (array_key_exists($key, $this->messageStoreKeys)) {
$state = $this->getState();
list($stateKey, $sourceKey) = $this->messageStoreKeys[$key];
$value = empty($sourceKey) ? $taskResult->getMessage() : $taskResult[$sourceKey];
$state[$stateKey] = $value;
}
}
public function storeState($task, $key, $source = '')
{
$this->messageStoreKeys[spl_object_hash($task)] = [$key, $source];
return $this;
}
public function deferTaskConfiguration($task, $functionName, $stateKey)
{
return $this->defer(
$task,
function ($task, $state) use ($functionName, $stateKey) {
$fn = [$task, $functionName];
$value = $state[$stateKey];
$fn($value);
}
);
}
/**
* Defer execution of a callback function until just before a task
* runs. Use this time to provide more settings for the task, e.g. from
* the collection's shared state, which is populated with the results
* of previous test runs.
*/
public function defer($task, $callback)
{
$this->deferredCallbacks[spl_object_hash($task)][] = $callback;
return $this;
}
protected function doDeferredInitialization($task)
{
// If the task is a state consumer, then call its receiveState method
if ($task instanceof \Robo\State\Consumer) {
$task->receiveState($this->getState());
}
// Check and see if there are any deferred callbacks for this task.
$key = spl_object_hash($task);
if (!array_key_exists($key, $this->deferredCallbacks)) {
return;
}
// Call all of the deferred callbacks
foreach ($this->deferredCallbacks[$key] as $fn) {
$fn($task, $this->getState());
}
}
/**
* @param TaskInterface|NestedCollectionInterface|WrappedTaskInterface $task
* @param $parentCollection
*/
protected function setParentCollectionForTask($task, $parentCollection)
{
if ($task instanceof NestedCollectionInterface) {
$task->setParentCollection($parentCollection);
}
}
/**
* Run all of the tasks in a provided list, ignoring failures.
* This is used to roll back or complete.
*
* @param TaskInterface[] $taskList
*/
protected function runTaskListIgnoringFailures(array $taskList)
{
foreach ($taskList as $task) {
try {
$this->runSubtask($task);
} catch (\Exception $e) {
// Ignore rollback failures.
}
}
}
/**
* Give all of our tasks to the provided collection builder.
*
* @param CollectionBuilder $builder
*/
public function transferTasks($builder)
{
foreach ($this->taskList as $name => $taskGroup) {
// TODO: We are abandoning all of our before and after tasks here.
// At the moment, transferTasks is only called under conditions where
// there will be none of these, but care should be taken if that changes.
$task = $taskGroup->getTask();
$builder->addTaskToCollection($task);
}
$this->reset();
}
}

View File

@@ -0,0 +1,571 @@
<?php
namespace Robo\Collection;
use Consolidation\Config\Inject\ConfigForSetters;
use Robo\Config\Config;
use Psr\Log\LogLevel;
use Robo\Contract\InflectionInterface;
use Robo\Contract\TaskInterface;
use Robo\Contract\CompletionInterface;
use Robo\Contract\WrappedTaskInterface;
use Robo\Task\Simulator;
use ReflectionClass;
use Robo\Task\BaseTask;
use Robo\Contract\BuilderAwareInterface;
use Robo\Contract\CommandInterface;
use Robo\Contract\VerbosityThresholdInterface;
use Robo\State\StateAwareInterface;
use Robo\State\StateAwareTrait;
use Robo\Result;
/**
* Creates a collection, and adds tasks to it. The collection builder
* offers a streamlined chained-initialization mechanism for easily
* creating task groups. Facilities for creating working and temporary
* directories are also provided.
*
* ``` php
* <?php
* $result = $this->collectionBuilder()
* ->taskFilesystemStack()
* ->mkdir('g')
* ->touch('g/g.txt')
* ->rollback(
* $this->taskDeleteDir('g')
* )
* ->taskFilesystemStack()
* ->mkdir('g/h')
* ->touch('g/h/h.txt')
* ->taskFilesystemStack()
* ->mkdir('g/h/i/c')
* ->touch('g/h/i/i.txt')
* ->run()
* ?>
*
* In the example above, the `taskDeleteDir` will be called if
* ```
*/
class CollectionBuilder extends BaseTask implements NestedCollectionInterface, WrappedTaskInterface, CommandInterface, StateAwareInterface
{
use StateAwareTrait;
/**
* @var \Robo\Tasks
*/
protected $commandFile;
/**
* @var CollectionInterface
*/
protected $collection;
/**
* @var TaskInterface
*/
protected $currentTask;
/**
* @var bool
*/
protected $simulated;
/**
* @param \Robo\Tasks $commandFile
*/
public function __construct($commandFile)
{
$this->commandFile = $commandFile;
$this->resetState();
}
public static function create($container, $commandFile)
{
$builder = new self($commandFile);
$builder->setLogger($container->get('logger'));
$builder->setProgressIndicator($container->get('progressIndicator'));
$builder->setConfig($container->get('config'));
$builder->setOutputAdapter($container->get('outputAdapter'));
return $builder;
}
/**
* @param bool $simulated
*
* @return $this
*/
public function simulated($simulated = true)
{
$this->simulated = $simulated;
return $this;
}
/**
* @return bool
*/
public function isSimulated()
{
if (!isset($this->simulated)) {
$this->simulated = $this->getConfig()->get(Config::SIMULATE);
}
return $this->simulated;
}
/**
* Create a temporary directory to work in. When the collection
* completes or rolls back, the temporary directory will be deleted.
* Returns the path to the location where the directory will be
* created.
*
* @param string $prefix
* @param string $base
* @param bool $includeRandomPart
*
* @return string
*/
public function tmpDir($prefix = 'tmp', $base = '', $includeRandomPart = true)
{
// n.b. Any task that the builder is asked to create is
// automatically added to the builder's collection, and
// wrapped in the builder object. Therefore, the result
// of any call to `taskFoo()` from within the builder will
// always be `$this`.
return $this->taskTmpDir($prefix, $base, $includeRandomPart)->getPath();
}
/**
* Create a working directory to hold results. A temporary directory
* is first created to hold the intermediate results. After the
* builder finishes, the work directory is moved into its final location;
* any results already in place will be moved out of the way and
* then deleted.
*
* @param string $finalDestination The path where the working directory
* will be moved once the task collection completes.
*
* @return string
*/
public function workDir($finalDestination)
{
// Creating the work dir task in this context adds it to our task collection.
return $this->taskWorkDir($finalDestination)->getPath();
}
public function addTask(TaskInterface $task)
{
$this->getCollection()->add($task);
return $this;
}
/**
* Add arbitrary code to execute as a task.
*
* @see \Robo\Collection\CollectionInterface::addCode
*
* @param callable $code
* @param int|string $name
* @return $this
*/
public function addCode(callable $code, $name = \Robo\Collection\CollectionInterface::UNNAMEDTASK)
{
$this->getCollection()->addCode($code, $name);
return $this;
}
/**
* Add a list of tasks to our task collection.
*
* @param TaskInterface[] $tasks
* An array of tasks to run with rollback protection
*
* @return $this
*/
public function addTaskList(array $tasks)
{
$this->getCollection()->addTaskList($tasks);
return $this;
}
public function rollback(TaskInterface $task)
{
// Ensure that we have a collection if we are going to add
// a rollback function.
$this->getCollection()->rollback($task);
return $this;
}
public function rollbackCode(callable $rollbackCode)
{
$this->getCollection()->rollbackCode($rollbackCode);
return $this;
}
public function completion(TaskInterface $task)
{
$this->getCollection()->completion($task);
return $this;
}
public function completionCode(callable $completionCode)
{
$this->getCollection()->completionCode($completionCode);
return $this;
}
/**
* @param string $text
* @param array $context
* @param string $level
*
* @return $this
*/
public function progressMessage($text, $context = [], $level = LogLevel::NOTICE)
{
$this->getCollection()->progressMessage($text, $context, $level);
return $this;
}
/**
* @param \Robo\Collection\NestedCollectionInterface $parentCollection
*
* @return $this
*/
public function setParentCollection(NestedCollectionInterface $parentCollection)
{
$this->getCollection()->setParentCollection($parentCollection);
return $this;
}
/**
* Called by the factory method of each task; adds the current
* task to the task builder.
*
* TODO: protected
*
* @param TaskInterface $task
*
* @return $this
*/
public function addTaskToCollection($task)
{
// Postpone creation of the collection until the second time
// we are called. At that time, $this->currentTask will already
// be populated. We call 'getCollection()' so that it will
// create the collection and add the current task to it.
// Note, however, that if our only tasks implements NestedCollectionInterface,
// then we should force this builder to use a collection.
if (!$this->collection && (isset($this->currentTask) || ($task instanceof NestedCollectionInterface))) {
$this->getCollection();
}
$this->currentTask = $task;
if ($this->collection) {
$this->collection->add($task);
}
return $this;
}
public function getState()
{
$collection = $this->getCollection();
return $collection->getState();
}
public function storeState($key, $source = '')
{
return $this->callCollectionStateFuntion(__FUNCTION__, func_get_args());
}
public function deferTaskConfiguration($functionName, $stateKey)
{
return $this->callCollectionStateFuntion(__FUNCTION__, func_get_args());
}
public function defer($callback)
{
return $this->callCollectionStateFuntion(__FUNCTION__, func_get_args());
}
protected function callCollectionStateFuntion($functionName, $args)
{
$currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
array_unshift($args, $currentTask);
$collection = $this->getCollection();
$fn = [$collection, $functionName];
call_user_func_array($fn, $args);
return $this;
}
public function setVerbosityThreshold($verbosityThreshold)
{
$currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
if ($currentTask) {
$currentTask->setVerbosityThreshold($verbosityThreshold);
return $this;
}
parent::setVerbosityThreshold($verbosityThreshold);
return $this;
}
/**
* Return the current task for this collection builder.
* TODO: Not needed?
*
* @return \Robo\Contract\TaskInterface
*/
public function getCollectionBuilderCurrentTask()
{
return $this->currentTask;
}
/**
* Create a new builder with its own task collection
*
* @return CollectionBuilder
*/
public function newBuilder()
{
$collectionBuilder = new self($this->commandFile);
$collectionBuilder->inflect($this);
$collectionBuilder->simulated($this->isSimulated());
$collectionBuilder->setVerbosityThreshold($this->verbosityThreshold());
$collectionBuilder->setState($this->getState());
return $collectionBuilder;
}
/**
* Calling the task builder with methods of the current
* task calls through to that method of the task.
*
* There is extra complexity in this function that could be
* simplified if we attached the 'LoadAllTasks' and custom tasks
* to the collection builder instead of the RoboFile. While that
* change would be a better design overall, it would require that
* the user do a lot more work to set up and use custom tasks.
* We therefore take on some additional complexity here in order
* to allow users to maintain their tasks in their RoboFile, which
* is much more convenient.
*
* Calls to $this->collectionBuilder()->taskFoo() cannot be made
* directly because all of the task methods are protected. These
* calls will therefore end up here. If the method name begins
* with 'task', then it is eligible to be used with the builder.
*
* When we call getBuiltTask, below, it will use the builder attached
* to the commandfile to build the task. However, this is not what we
* want: the task needs to be built from THIS collection builder, so that
* it will be affected by whatever state is active in this builder.
* To do this, we have two choices: 1) save and restore the builder
* in the commandfile, or 2) clone the commandfile and set this builder
* on the copy. 1) is vulnerable to failure in multithreaded environments
* (currently not supported), while 2) might cause confusion if there
* is shared state maintained in the commandfile, which is in the
* domain of the user.
*
* Note that even though we are setting up the commandFile to
* use this builder, getBuiltTask always creates a new builder
* (which is constructed using all of the settings from the
* commandFile's builder), and the new task is added to that.
* We therefore need to transfer the newly built task into this
* builder. The temporary builder is discarded.
*
* @param string $fn
* @param array $args
*
* @return $this|mixed
*/
public function __call($fn, $args)
{
if (preg_match('#^task[A-Z]#', $fn) && (method_exists($this->commandFile, 'getBuiltTask'))) {
$saveBuilder = $this->commandFile->getBuilder();
$this->commandFile->setBuilder($this);
$temporaryBuilder = $this->commandFile->getBuiltTask($fn, $args);
$this->commandFile->setBuilder($saveBuilder);
if (!$temporaryBuilder) {
throw new \BadMethodCallException("No such method $fn: task does not exist in " . get_class($this->commandFile));
}
$temporaryBuilder->getCollection()->transferTasks($this);
return $this;
}
if (!isset($this->currentTask)) {
throw new \BadMethodCallException("No such method $fn: current task undefined in collection builder.");
}
// If the method called is a method of the current task,
// then call through to the current task's setter method.
$result = call_user_func_array([$this->currentTask, $fn], $args);
// If something other than a setter method is called, then return its result.
$currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
if (isset($result) && ($result !== $currentTask)) {
return $result;
}
return $this;
}
/**
* Construct the desired task and add it to this builder.
*
* @param string|object $name
* @param array $args
*
* @return \Robo\Collection\CollectionBuilder
*/
public function build($name, $args)
{
$reflection = new ReflectionClass($name);
$task = $reflection->newInstanceArgs($args);
if (!$task) {
throw new RuntimeException("Can not construct task $name");
}
$task = $this->fixTask($task, $args);
$this->configureTask($name, $task);
return $this->addTaskToCollection($task);
}
/**
* @param InflectionInterface $task
* @param array $args
*
* @return \Robo\Collection\CompletionWrapper|\Robo\Task\Simulator
*/
protected function fixTask($task, $args)
{
if ($task instanceof InflectionInterface) {
$task->inflect($this);
}
if ($task instanceof BuilderAwareInterface) {
$task->setBuilder($this);
}
if ($task instanceof VerbosityThresholdInterface) {
$task->setVerbosityThreshold($this->verbosityThreshold());
}
// Do not wrap our wrappers.
if ($task instanceof CompletionWrapper || $task instanceof Simulator) {
return $task;
}
// Remember whether or not this is a task before
// it gets wrapped in any decorator.
$isTask = $task instanceof TaskInterface;
$isCollection = $task instanceof NestedCollectionInterface;
// If the task implements CompletionInterface, ensure
// that its 'complete' method is called when the application
// terminates -- but only if its 'run' method is called
// first. If the task is added to a collection, then the
// task will be unwrapped via its `original` method, and
// it will be re-wrapped with a new completion wrapper for
// its new collection.
if ($task instanceof CompletionInterface) {
$task = new CompletionWrapper(Temporary::getCollection(), $task);
}
// If we are in simulated mode, then wrap any task in
// a TaskSimulator.
if ($isTask && !$isCollection && ($this->isSimulated())) {
$task = new \Robo\Task\Simulator($task, $args);
$task->inflect($this);
}
return $task;
}
/**
* Check to see if there are any setter methods defined in configuration
* for this task.
*/
protected function configureTask($taskClass, $task)
{
$taskClass = static::configClassIdentifier($taskClass);
$configurationApplier = new ConfigForSetters($this->getConfig(), $taskClass, 'task.');
$configurationApplier->apply($task, 'settings');
// TODO: If we counted each instance of $taskClass that was called from
// this builder, then we could also apply configuration from
// "task.{$taskClass}[$N].settings"
// TODO: If the builder knew what the current command name was,
// then we could also search for task configuration under
// command-specific keys such as "command.{$commandname}.task.{$taskClass}.settings".
}
/**
* When we run the collection builder, run everything in the collection.
*
* @return \Robo\Result
*/
public function run()
{
$this->startTimer();
$result = $this->runTasks();
$this->stopTimer();
$result['time'] = $this->getExecutionTime();
$result->mergeData($this->getState()->getData());
return $result;
}
/**
* If there is a single task, run it; if there is a collection, run
* all of its tasks.
*
* @return \Robo\Result
*/
protected function runTasks()
{
if (!$this->collection && $this->currentTask) {
$result = $this->currentTask->run();
return Result::ensureResult($this->currentTask, $result);
}
return $this->getCollection()->run();
}
/**
* @return string
*/
public function getCommand()
{
if (!$this->collection && $this->currentTask) {
$task = $this->currentTask;
$task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
if ($task instanceof CommandInterface) {
return $task->getCommand();
}
}
return $this->getCollection()->getCommand();
}
/**
* @return \Robo\Collection\Collection
*/
public function original()
{
return $this->getCollection();
}
/**
* Return the collection of tasks associated with this builder.
*
* @return CollectionInterface
*/
public function getCollection()
{
if (!isset($this->collection)) {
$this->collection = new Collection();
$this->collection->inflect($this);
$this->collection->setState($this->getState());
$this->collection->setProgressBarAutoDisplayInterval($this->getConfig()->get(Config::PROGRESS_BAR_AUTO_DISPLAY_INTERVAL));
if (isset($this->currentTask)) {
$this->collection->add($this->currentTask);
}
}
return $this->collection;
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Robo\Collection;
use Psr\Log\LogLevel;
use Robo\Contract\TaskInterface;
interface CollectionInterface extends NestedCollectionInterface
{
/**
* Unnamed tasks are assigned an arbitrary numeric index
* in the task list. Any numeric value may be used, but the
* UNNAMEDTASK constant is recommended for clarity.
*
* @var int
*/
const UNNAMEDTASK = 0;
/**
* Add a task or a list of tasks to our task collection. Each task
* will run via its 'run()' method once (and if) all of the tasks
* added before it complete successfully. If the task also implements
* RollbackInterface, then it will be rolled back via its 'rollback()'
* method ONLY if its 'run()' method completes successfully, and some
* task added after it fails.
*
* @param TaskInterface $task
* The task to add to our collection.
* @param int|string $name
* An optional name for the task -- missing or UNNAMEDTASK for unnamed tasks.
* Names are used for positioning before and after tasks.
*
* @return CollectionInterface
*/
public function add(TaskInterface $task, $name = self::UNNAMEDTASK);
/**
* Add arbitrary code to execute as a task.
*
* @param callable $code Code to execute as a task
* @param int|string $name
* An optional name for the task -- missing or UNNAMEDTASK for unnamed tasks.
* Names are used for positioning before and after tasks.
*
* @return $this
*/
public function addCode(callable $code, $name = self::UNNAMEDTASK);
/**
* Add arbitrary code that will be called once for every item in the
* provided array or iterable object. If the function result of the
* provided callback is a TaskInterface or Collection, then it will be
* executed.
*
* @param CollectionInterface|array $iterable A collection of things to iterate
* @param $code $code A callback function to call for each item in the collection.
*
* @return $this
*/
public function addIterable($iterable, callable $code);
/**
* Add a rollback task to our task collection. A rollback task
* will execute ONLY if all of the tasks added before it complete
* successfully, AND some task added after it fails.
*
* @param TaskInterface $rollbackTask
* The rollback task to add. Note that the 'run()' method of the
* task executes, not its 'rollback()' method. To use the 'rollback()'
* method, add the task via 'Collection::add()' instead.
*
* @return $this
*/
public function rollback(TaskInterface $rollbackTask);
/**
* Add arbitrary code to execute as a rollback.
*
* @param callable $rollbackTask Code to execute during rollback processing
*
* @return $this
*/
public function rollbackCode(callable $rollbackTask);
/**
* Add a completion task to our task collection. A completion task
* will execute EITHER after all tasks succeed, OR immediatley after
* any task fails. Completion tasks never cause errors to be returned
* from Collection::run(), even if they fail.
*
* @param TaskInterface $completionTask
* The completion task to add. Note that the 'run()' method of the
* task executes, just as if the task was added normally.
*
* @return $this
*/
public function completion(TaskInterface $completionTask);
/**
* Add arbitrary code to execute as a completion.
*
* @param callable $completionTask Code to execute after collection completes
*
* @return $this
*/
public function completionCode(callable $completionTask);
/**
* Add a task before an existing named task.
*
* @param string $name
* The name of the task to insert before. The named task MUST exist.
* @param callable|TaskInterface $task
* The task to add.
* @param int|string $nameOfTaskToAdd
* The name of the task to add. If not provided, will be associated
* with the named task it was added before.
*
* @return $this
*/
public function before($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK);
/**
* Add a task after an existing named task.
*
* @param string $name
* The name of the task to insert before. The named task MUST exist.
* @param callable|TaskInterface $task
* The task to add.
* @param int|string $nameOfTaskToAdd
* The name of the task to add. If not provided, will be associated
* with the named task it was added after.
*
* @return $this
*/
public function after($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK);
/**
* Print a progress message after Collection::run() has executed
* all of the tasks that were added prior to the point when this
* method was called. If one of the previous tasks fail, then this
* message will not be printed.
*
* @param string $text Message to print.
* @param array $context Extra context data for use by the logger. Note
* that the data from the collection state is merged with the provided context.
* @param \Psr\Log\LogLevel|string $level The log level to print the information at. Default is NOTICE.
*
* @return $this
*/
public function progressMessage($text, $context = [], $level = LogLevel::NOTICE);
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Robo\Collection;
use Consolidation\AnnotatedCommand\Hooks\ProcessResultInterface;
use Consolidation\AnnotatedCommand\CommandData;
use Robo\Contract\TaskInterface;
use Robo\Result;
/**
* The collection process hook is added to the annotation command
* hook manager in Runner::configureContainer(). This hook will be
* called every time a command runs. If the command result is a
* \Robo\Contract\TaskInterface (in particular, \Robo\Collection\Collection),
* then we run the collection, and return the result. We ignore results
* of any other type.
*/
class CollectionProcessHook implements ProcessResultInterface
{
/**
* @param \Robo\Result|\Robo\Contract\TaskInterface $result
* @param \Consolidation\AnnotatedCommand\CommandData $commandData
*
* @return null|\Robo\Result
*/
public function process($result, CommandData $commandData)
{
if ($result instanceof TaskInterface) {
try {
return $result->run();
} catch (\Exception $e) {
return Result::fromException($result, $e);
}
}
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace Robo\Collection;
use Robo\Task\BaseTask;
use Robo\Contract\TaskInterface;
use Robo\Contract\RollbackInterface;
use Robo\Contract\CompletionInterface;
use Robo\Contract\WrappedTaskInterface;
/**
* Creates a task wrapper that will manage rollback and collection
* management to a task when it runs. Tasks are automatically
* wrapped in a CompletionWrapper when added to a task collection.
*
* Clients may need to wrap their task in a CompletionWrapper if it
* creates temporary objects.
*
* @see \Robo\Task\Filesystem\loadTasks::taskTmpDir
*/
class CompletionWrapper extends BaseTask implements WrappedTaskInterface
{
/**
* @var \Robo\Collection\Collection
*/
private $collection;
/**
* @var \Robo\Contract\TaskInterface
*/
private $task;
/**
* @var NULL|\Robo\Contract\TaskInterface
*/
private $rollbackTask;
/**
* Create a CompletionWrapper.
*
* Temporary tasks are always wrapped in a CompletionWrapper, as are
* any tasks that are added to a collection. If a temporary task
* is added to a collection, then it is first unwrapped from its
* CompletionWrapper (via its original() method), and then added to a
* new CompletionWrapper for the collection it is added to.
*
* In this way, when the CompletionWrapper is finally executed, the
* task's rollback and completion handlers will be registered on
* whichever collection it was registered on.
*
* @todo Why not CollectionInterface the type of the $collection argument?
*
* @param \Robo\Collection\Collection $collection
* @param \Robo\Contract\TaskInterface $task
* @param \Robo\Contract\TaskInterface|NULL $rollbackTask
*/
public function __construct(Collection $collection, TaskInterface $task, TaskInterface $rollbackTask = null)
{
$this->collection = $collection;
$this->task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
$this->rollbackTask = $rollbackTask;
}
/**
* {@inheritdoc}
*/
public function original()
{
return $this->task;
}
/**
* Before running this task, register its rollback and completion
* handlers on its collection. The reason this class exists is to
* defer registration of rollback and completion tasks until 'run()' time.
*
* @return \Robo\Result
*/
public function run()
{
if ($this->rollbackTask) {
$this->collection->registerRollback($this->rollbackTask);
}
if ($this->task instanceof RollbackInterface) {
$this->collection->registerRollback(new CallableTask([$this->task, 'rollback'], $this->task));
}
if ($this->task instanceof CompletionInterface) {
$this->collection->registerCompletion(new CallableTask([$this->task, 'complete'], $this->task));
}
return $this->task->run();
}
/**
* Make this wrapper object act like the class it wraps.
*
* @param string $function
* @param array $args
*
* @return mixed
*/
public function __call($function, $args)
{
return call_user_func_array(array($this->task, $function), $args);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Robo\Collection;
use Robo\Contract\TaskInterface;
use Robo\Contract\WrappedTaskInterface;
use Robo\Contract\ProgressIndicatorAwareInterface;
/**
* One element in a collection. Each element consists of a task
* all of its before tasks, and all of its after tasks.
*
* This class is internal to Collection; it should not be used directly.
*/
class Element
{
/**
* @var \Robo\Contract\TaskInterface
*/
protected $task;
/**
* @var array
*/
protected $before = [];
/**
* @var array
*/
protected $after = [];
public function __construct(TaskInterface $task)
{
$this->task = $task;
}
/**
* @param mixed $before
* @param string $name
*/
public function before($before, $name)
{
if ($name) {
$this->before[$name] = $before;
} else {
$this->before[] = $before;
}
}
/**
* @param mixed $after
* @param string $name
*/
public function after($after, $name)
{
if ($name) {
$this->after[$name] = $after;
} else {
$this->after[] = $after;
}
}
/**
* @return array
*/
public function getBefore()
{
return $this->before;
}
/**
* @return array
*/
public function getAfter()
{
return $this->after;
}
/**
* @return \Robo\Contract\TaskInterface
*/
public function getTask()
{
return $this->task;
}
/**
* @return array
*/
public function getTaskList()
{
return array_merge($this->getBefore(), [$this->getTask()], $this->getAfter());
}
/**
* @return int
*/
public function progressIndicatorSteps()
{
$steps = 0;
foreach ($this->getTaskList() as $task) {
if ($task instanceof WrappedTaskInterface) {
$task = $task->original();
}
// If the task is a ProgressIndicatorAwareInterface, then it
// will advance the progress indicator a number of times.
if ($task instanceof ProgressIndicatorAwareInterface) {
$steps += $task->progressIndicatorSteps();
}
// We also advance the progress indicator once regardless
// of whether it is progress-indicator aware or not.
$steps++;
}
return $steps;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Robo\Collection;
interface NestedCollectionInterface
{
/**
* @param \Robo\Collection\NestedCollectionInterface $parentCollection
*
* @return $this
*/
public function setParentCollection(NestedCollectionInterface $parentCollection);
}

View File

@@ -0,0 +1,196 @@
<?php
namespace Robo\Collection;
use Robo\Result;
use Robo\TaskInfo;
use Robo\Task\BaseTask;
use Robo\Contract\BuilderAwareInterface;
use Robo\Common\BuilderAwareTrait;
/**
* Creates a task wrapper that converts any Callable into an
* object that will execute the callback once for each item in the
* provided collection.
*
* It is not necessary to use this class directly; Collection::addIterable
* will automatically create one when it is called.
*/
class TaskForEach extends BaseTask implements NestedCollectionInterface, BuilderAwareInterface
{
use BuilderAwareTrait;
/**
* @var callable[]
*/
protected $functionStack = [];
/**
* @var callable[]
*/
protected $countingStack = [];
/**
* @var string
*/
protected $message;
/**
* @var array
*/
protected $context = [];
protected $iterable;
/**
* @var \Robo\Collection\NestedCollectionInterface
*/
protected $parentCollection;
public function __construct($iterable)
{
$this->iterable = $iterable;
}
/**
* @param string $message
* @param array $context
*
* @return $this
*/
public function iterationMessage($message, $context = [])
{
$this->message = $message;
$this->context = $context + ['name' => 'Progress'];
return $this;
}
/**
* @param int|string $key
* @param mixed $value
*/
protected function showIterationMessage($key, $value)
{
if ($this->message) {
$context = ['key' => $key, 'value' => $value];
$context += $this->context;
$context += TaskInfo::getTaskContext($this);
$this->printTaskInfo($this->message, $context);
}
}
/**
* @param callable $fn
*
* @return $this
*/
public function withEachKeyValueCall(callable $fn)
{
$this->functionStack[] = $fn;
return $this;
}
/**
* @param callable $fn
*
* @return \Robo\Collection\TaskForEach
*/
public function call(callable $fn)
{
return $this->withEachKeyValueCall(
function ($key, $value) use ($fn) {
return call_user_func($fn, $value);
}
);
}
/**
* @param callable $fn
*
* @return \Robo\Collection\TaskForEach
*/
public function withBuilder(callable $fn)
{
$this->countingStack[] =
function ($key, $value) use ($fn) {
// Create a new builder for every iteration
$builder = $this->collectionBuilder();
// The user function should build task operations using
// the $key / $value parameters; we will call run() on
// the builder thus constructed.
call_user_func($fn, $builder, $key, $value);
return $builder->getCollection()->progressIndicatorSteps();
};
return $this->withEachKeyValueCall(
function ($key, $value) use ($fn) {
// Create a new builder for every iteration
$builder = $this->collectionBuilder()
->setParentCollection($this->parentCollection);
// The user function should build task operations using
// the $key / $value parameters; we will call run() on
// the builder thus constructed.
call_user_func($fn, $builder, $key, $value);
return $builder->run();
}
);
}
/**
* {@inheritdoc}
*/
public function setParentCollection(NestedCollectionInterface $parentCollection)
{
$this->parentCollection = $parentCollection;
return $this;
}
/**
* {@inheritdoc}
*/
public function progressIndicatorSteps()
{
$multiplier = count($this->functionStack);
if (!empty($this->countingStack)) {
$value = reset($this->iterable);
$key = key($this->iterable);
foreach ($this->countingStack as $fn) {
$multiplier += call_user_func($fn, $key, $value);
}
}
return count($this->iterable) * $multiplier;
}
/**
* {@inheritdoc}
*/
public function run()
{
$finalResult = Result::success($this);
$this->startProgressIndicator();
foreach ($this->iterable as $key => $value) {
$this->showIterationMessage($key, $value);
try {
foreach ($this->functionStack as $fn) {
$result = call_user_func($fn, $key, $value);
$this->advanceProgressIndicator();
if (!isset($result)) {
$result = Result::success($this);
}
// If the function returns a result, it must either return
// a \Robo\Result or an exit code. In the later case, we
// convert it to a \Robo\Result.
if (!$result instanceof Result) {
$result = new Result($this, $result);
}
if (!$result->wasSuccessful()) {
return $result;
}
$finalResult = $result->merge($finalResult);
}
} catch (\Exception $e) {
return Result::fromException($result, $e);
}
}
$this->stopProgressIndicator();
return $finalResult;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Robo\Collection;
/**
* The temporary collection keeps track of the global collection of
* temporary cleanup tasks in instances where temporary-generating
* tasks are executed directly via their run() method, rather than
* as part of a collection.
*
* In general, temporary-generating tasks should always be run in
* a collection, as the cleanup functions registered with the
* Temporary collection will not run until requested.
*
* Since the results could be undefined if cleanup functions were called
* at arbitrary times during a program's execution, cleanup should only
* be done immeidately prior to program termination, when there is no
* danger of cleaning up after some unrelated task.
*
* An application need never use Temporary directly, save to
* call Temporary::wrap() inside loadTasks or loadShortcuts, and
* to call Temporary::complete() immediately prior to terminating.
* This is recommended, but not required; this function will be
* registered as a shutdown function, and called on termination.
*/
class Temporary
{
private static $collection;
/**
* Provides direct access to the collection of temporaries, if necessary.
*/
public static function getCollection()
{
if (!static::$collection) {
static::$collection = \Robo\Robo::getContainer()->get('collection');
register_shutdown_function(function () {
static::complete();
});
}
return static::$collection;
}
/**
* Call the complete method of all of the registered objects.
*/
public static function complete()
{
// Run the collection of tasks. This will also run the
// completion tasks.
$collection = static::getCollection();
$collection->run();
// Make sure that our completion functions do not run twice.
$collection->reset();
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Robo\Collection;
trait loadTasks
{
/**
* Run a callback function on each item in a collection
*
* @param array $collection
*
* @return \Robo\Collection\TaskForEach
*/
protected function taskForEach($collection)
{
return $this->task(TaskForEach::class, $collection);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Robo\Common;
use Robo\Collection\CollectionBuilder;
trait BuilderAwareTrait
{
/**
* @var \Robo\Collection\CollectionBuilder
*/
protected $builder;
/**
* @see \Robo\Contract\BuilderAwareInterface::setBuilder()
*
* @param \Robo\Collection\CollectionBuilder $builder
*
* @return $this
*/
public function setBuilder(CollectionBuilder $builder)
{
$this->builder = $builder;
return $this;
}
/**
* @see \Robo\Contract\BuilderAwareInterface::getBuilder()
*
* @return \Robo\Collection\CollectionBuilder
*/
public function getBuilder()
{
return $this->builder;
}
/**
* @return \Robo\Collection\CollectionBuilder
*/
protected function collectionBuilder()
{
return $this->getBuilder()->newBuilder();
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace Robo\Common;
use Robo\Common\ProcessUtils;
/**
* Use this to add arguments and options to the $arguments property.
*/
trait CommandArguments
{
/**
* @var string
*/
protected $arguments = '';
/**
* Pass argument to executable. Its value will be automatically escaped.
*
* @param string $arg
*
* @return $this
*/
public function arg($arg)
{
return $this->args($arg);
}
/**
* Pass methods parameters as arguments to executable. Argument values
* are automatically escaped.
*
* @param string|string[] $args
*
* @return $this
*/
public function args($args)
{
if (!is_array($args)) {
$args = func_get_args();
}
$this->arguments .= ' ' . implode(' ', array_map('static::escape', $args));
return $this;
}
/**
* Pass the provided string in its raw (as provided) form as an argument to executable.
*
* @param string $arg
*
* @return $this
*/
public function rawArg($arg)
{
$this->arguments .= " $arg";
return $this;
}
/**
* Escape the provided value, unless it contains only alphanumeric
* plus a few other basic characters.
*
* @param string $value
*
* @return string
*/
public static function escape($value)
{
if (preg_match('/^[a-zA-Z0-9\/\.@~_-]+$/', $value)) {
return $value;
}
return ProcessUtils::escapeArgument($value);
}
/**
* Pass option to executable. Options are prefixed with `--` , value can be provided in second parameter.
* Option values are automatically escaped.
*
* @param string $option
* @param string $value
* @param string $separator
*
* @return $this
*/
public function option($option, $value = null, $separator = ' ')
{
if ($option !== null and strpos($option, '-') !== 0) {
$option = "--$option";
}
$this->arguments .= null == $option ? '' : " " . $option;
$this->arguments .= null == $value ? '' : $separator . static::escape($value);
return $this;
}
/**
* Pass multiple options to executable. The associative array contains
* the key:value pairs that become `--key value`, for each item in the array.
* Values are automatically escaped.
*/
public function options(array $options, $separator = ' ')
{
foreach ($options as $option => $value) {
$this->option($option, $value, $separator);
}
return $this;
}
/**
* Pass an option with multiple values to executable. Value can be a string or array.
* Option values are automatically escaped.
*
* @param string $option
* @param string|array $value
* @param string $separator
*
* @return $this
*/
public function optionList($option, $value = array(), $separator = ' ')
{
if (is_array($value)) {
foreach ($value as $item) {
$this->optionList($option, $item, $separator);
}
} else {
$this->option($option, $value, $separator);
}
return $this;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Robo\Common;
use Robo\Contract\CommandInterface;
use Robo\Exception\TaskException;
/**
* This task can receive commands from task implementing CommandInterface.
*/
trait CommandReceiver
{
/**
* @param string|\Robo\Contract\CommandInterface $command
*
* @return string
*
* @throws \Robo\Exception\TaskException
*/
protected function receiveCommand($command)
{
if (!is_object($command)) {
return $command;
}
if ($command instanceof CommandInterface) {
return $command->getCommand();
} else {
throw new TaskException($this, get_class($command) . " does not implement CommandInterface, so can't be passed into this task");
}
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Robo\Common;
use Robo\Robo;
use Consolidation\Config\ConfigInterface;
trait ConfigAwareTrait
{
/**
* @var ConfigInterface
*/
protected $config;
/**
* Set the config management object.
*
* @param ConfigInterface $config
*
* @return $this
*
* @see \Robo\Contract\ConfigAwareInterface::setConfig()
*/
public function setConfig(ConfigInterface $config)
{
$this->config = $config;
return $this;
}
/**
* Get the config management object.
*
* @return ConfigInterface
*
* @see \Robo\Contract\ConfigAwareInterface::getConfig()
*/
public function getConfig()
{
return $this->config;
}
/**
* Any class that uses ConfigAwareTrait SHOULD override this method
* , and define a prefix for its configuration items. This is usually
* done in a base class. When used, this method should return a string
* that ends with a "."; see BaseTask::configPrefix().
*
* @return string
*/
protected static function configPrefix()
{
return '';
}
protected static function configClassIdentifier($classname)
{
$configIdentifier = strtr($classname, '\\', '.');
$configIdentifier = preg_replace('#^(.*\.Task\.|\.)#', '', $configIdentifier);
return $configIdentifier;
}
protected static function configPostfix()
{
return '';
}
/**
* @param string $key
*
* @return string
*/
private static function getClassKey($key)
{
$configPrefix = static::configPrefix(); // task.
$configClass = static::configClassIdentifier(get_called_class()); // PARTIAL_NAMESPACE.CLASSNAME
$configPostFix = static::configPostfix(); // .settings
return sprintf('%s%s%s.%s', $configPrefix, $configClass, $configPostFix, $key);
}
/**
* @param string $key
* @param mixed $value
* @param Config|null $config
*/
public static function configure($key, $value, $config = null)
{
if (!$config) {
$config = Robo::config();
}
$config->setDefault(static::getClassKey($key), $value);
}
/**
* @param string $key
* @param mixed|null $default
*
* @return mixed|null
*/
protected function getConfigValue($key, $default = null)
{
if (!$this->getConfig()) {
return $default;
}
return $this->getConfig()->get(static::getClassKey($key), $default);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Robo\Common;
/**
* Simplifies generating of configuration chanined methods.
* You can only define configuration properties and use magic methods to set them.
* Methods will be named the same way as properties.
* * Boolean properties are switched on/off if no values is provided.
* * Array properties can accept non-array values, in this case value will be appended to array.
* You should also define phpdoc for methods.
*/
trait DynamicParams
{
/**
* @param string $property
* @param array $args
*
* @return $this
*/
public function __call($property, $args)
{
if (!property_exists($this, $property)) {
throw new \RuntimeException("Property $property in task ".get_class($this).' does not exists');
}
// toggle boolean values
if (!isset($args[0]) and (is_bool($this->$property))) {
$this->$property = !$this->$property;
return $this;
}
// append item to array
if (is_array($this->$property)) {
if (is_array($args[0])) {
$this->$property = $args[0];
} else {
array_push($this->$property, $args[0]);
}
return $this;
}
$this->$property = $args[0];
return $this;
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace Robo\Common;
use Robo\Result;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;
/**
* This task is supposed to be executed as shell command.
* You can specify working directory and if output is printed.
*/
trait ExecCommand
{
use ExecTrait;
/**
* @var \Robo\Common\TimeKeeper
*/
protected $execTimer;
/**
* @return \Robo\Common\TimeKeeper
*/
protected function getExecTimer()
{
if (!isset($this->execTimer)) {
$this->execTimer = new TimeKeeper();
}
return $this->execTimer;
}
/**
* Look for a "{$cmd}.phar" in the current working
* directory; return a string to exec it if it is
* found. Otherwise, look for an executable command
* of the same name via findExecutable.
*
* @param string $cmd
*
* @return bool|string
*/
protected function findExecutablePhar($cmd)
{
if (file_exists("{$cmd}.phar")) {
return "php {$cmd}.phar";
}
return $this->findExecutable($cmd);
}
/**
* Return the best path to the executable program
* with the provided name. Favor vendor/bin in the
* current project. If not found there, use
* whatever is on the $PATH.
*
* @param string $cmd
*
* @return bool|string
*/
protected function findExecutable($cmd)
{
$pathToCmd = $this->searchForExecutable($cmd);
if ($pathToCmd) {
return $this->useCallOnWindows($pathToCmd);
}
return false;
}
/**
* @param string $cmd
*
* @return string
*/
private function searchForExecutable($cmd)
{
$projectBin = $this->findProjectBin();
$localComposerInstallation = $projectBin . DIRECTORY_SEPARATOR . $cmd;
if (file_exists($localComposerInstallation)) {
return $localComposerInstallation;
}
$finder = new ExecutableFinder();
return $finder->find($cmd, null, []);
}
/**
* @return bool|string
*/
protected function findProjectBin()
{
$cwd = getcwd();
$candidates = [ __DIR__ . '/../../vendor/bin', __DIR__ . '/../../bin', $cwd . '/vendor/bin' ];
// If this project is inside a vendor directory, give highest priority
// to that directory.
$vendorDirContainingUs = realpath(__DIR__ . '/../../../..');
if (is_dir($vendorDirContainingUs) && (basename($vendorDirContainingUs) == 'vendor')) {
array_unshift($candidates, $vendorDirContainingUs . '/bin');
}
foreach ($candidates as $dir) {
if (is_dir("$dir")) {
return realpath($dir);
}
}
return false;
}
/**
* Wrap Windows executables in 'call' per 7a88757d
*
* @param string $cmd
*
* @return string
*/
protected function useCallOnWindows($cmd)
{
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
if (file_exists("{$cmd}.bat")) {
$cmd = "{$cmd}.bat";
}
return "call $cmd";
}
return $cmd;
}
protected function getCommandDescription()
{
return $this->process->getCommandLine();
}
/**
* @param string $command
*
* @return \Robo\Result
*/
protected function executeCommand($command)
{
// TODO: Symfony 4 requires that we supply the working directory.
$result_data = $this->execute(new Process($command, getcwd()));
return new Result(
$this,
$result_data->getExitCode(),
$result_data->getMessage(),
$result_data->getData()
);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Robo\Common;
/**
* This task specifies exactly one shell command.
* It can take additional arguments and options as config parameters.
*/
trait ExecOneCommand
{
use ExecCommand;
use CommandArguments;
}

View File

@@ -0,0 +1,408 @@
<?php
namespace Robo\Common;
use Robo\ResultData;
use Symfony\Component\Process\Process;
/**
* Class ExecTrait
* @package Robo\Common
*/
trait ExecTrait
{
/**
* @var bool
*/
protected $background = false;
/**
* @var null|int
*/
protected $timeout = null;
/**
* @var null|int
*/
protected $idleTimeout = null;
/**
* @var null|array
*/
protected $env = null;
/**
* @var Process
*/
protected $process;
/**
* @var resource|string
*/
protected $input;
/**
* @var boolean
*/
protected $interactive = null;
/**
* @var bool
*/
protected $isPrinted = true;
/**
* @var bool
*/
protected $isMetadataPrinted = true;
/**
* @var string
*/
protected $workingDirectory;
/**
* @return string
*/
abstract public function getCommandDescription();
/** Typically provided by Timer trait via ProgressIndicatorAwareTrait. */
abstract public function startTimer();
abstract public function stopTimer();
abstract public function getExecutionTime();
/**
* Typically provided by TaskIO Trait.
*/
abstract public function hideTaskProgress();
abstract public function showTaskProgress($inProgress);
abstract public function printTaskInfo($text, $context = null);
/**
* Typically provided by VerbosityThresholdTrait.
*/
abstract public function verbosityMeetsThreshold();
abstract public function writeMessage($message);
/**
* Sets $this->interactive() based on posix_isatty().
*
* @return $this
*/
public function detectInteractive()
{
// If the caller did not explicity set the 'interactive' mode,
// and output should be produced by this task (verbosityMeetsThreshold),
// then we will automatically set interactive mode based on whether
// or not output was redirected when robo was executed.
if (!isset($this->interactive) && function_exists('posix_isatty') && $this->verbosityMeetsThreshold()) {
$this->interactive = posix_isatty(STDOUT);
}
return $this;
}
/**
* Executes command in background mode (asynchronously)
*
* @return $this
*/
public function background($arg = true)
{
$this->background = $arg;
return $this;
}
/**
* Stop command if it runs longer then $timeout in seconds
*
* @param int $timeout
*
* @return $this
*/
public function timeout($timeout)
{
$this->timeout = $timeout;
return $this;
}
/**
* Stops command if it does not output something for a while
*
* @param int $timeout
*
* @return $this
*/
public function idleTimeout($timeout)
{
$this->idleTimeout = $timeout;
return $this;
}
/**
* Set a single environment variable, or multiple.
*/
public function env($env, $value = null)
{
if (!is_array($env)) {
$env = [$env => ($value ? $value : true)];
}
return $this->envVars($env);
}
/**
* Sets the environment variables for the command
*
* @param array $env
*
* @return $this
*/
public function envVars(array $env)
{
$this->env = $env;
return $this;
}
/**
* Pass an input to the process. Can be resource created with fopen() or string
*
* @param resource|string $input
*
* @return $this
*/
public function setInput($input)
{
$this->input = $input;
return $this;
}
/**
* Attach tty to process for interactive input
*
* @param $interactive bool
*
* @return $this
*/
public function interactive($interactive = true)
{
$this->interactive = $interactive;
return $this;
}
/**
* Is command printing its output to screen
*
* @return bool
*/
public function getPrinted()
{
return $this->isPrinted;
}
/**
* Changes working directory of command
*
* @param string $dir
*
* @return $this
*/
public function dir($dir)
{
$this->workingDirectory = $dir;
return $this;
}
/**
* Shortcut for setting isPrinted() and isMetadataPrinted() to false.
*
* @param bool $arg
*
* @return $this
*/
public function silent($arg)
{
if (is_bool($arg)) {
$this->isPrinted = !$arg;
$this->isMetadataPrinted = !$arg;
}
return $this;
}
/**
* Should command output be printed
*
* @param bool $arg
*
* @return $this
*
* @deprecated
*/
public function printed($arg)
{
$this->logger->warning("printed() is deprecated. Please use printOutput().");
return $this->printOutput($arg);
}
/**
* Should command output be printed
*
* @param bool $arg
*
* @return $this
*/
public function printOutput($arg)
{
if (is_bool($arg)) {
$this->isPrinted = $arg;
}
return $this;
}
/**
* Should command metadata be printed. I,e., command and timer.
*
* @param bool $arg
*
* @return $this
*/
public function printMetadata($arg)
{
if (is_bool($arg)) {
$this->isMetadataPrinted = $arg;
}
return $this;
}
/**
* @param Process $process
* @param callable $output_callback
*
* @return \Robo\ResultData
*/
protected function execute($process, $output_callback = null)
{
$this->process = $process;
if (!$output_callback) {
$output_callback = function ($type, $buffer) {
$progressWasVisible = $this->hideTaskProgress();
$this->writeMessage($buffer);
$this->showTaskProgress($progressWasVisible);
};
}
$this->detectInteractive();
if ($this->isMetadataPrinted) {
$this->printAction();
}
$this->process->setTimeout($this->timeout);
$this->process->setIdleTimeout($this->idleTimeout);
if ($this->workingDirectory) {
$this->process->setWorkingDirectory($this->workingDirectory);
}
if ($this->input) {
$this->process->setInput($this->input);
}
if ($this->interactive && $this->isPrinted) {
$this->process->setTty(true);
}
if (isset($this->env)) {
$this->process->setEnv($this->env);
}
if (!$this->background && !$this->isPrinted) {
$this->startTimer();
$this->process->run();
$this->stopTimer();
$output = rtrim($this->process->getOutput());
return new ResultData(
$this->process->getExitCode(),
$output,
$this->getResultData()
);
}
if (!$this->background && $this->isPrinted) {
$this->startTimer();
$this->process->run($output_callback);
$this->stopTimer();
return new ResultData(
$this->process->getExitCode(),
$this->process->getOutput(),
$this->getResultData()
);
}
try {
$this->process->start();
} catch (\Exception $e) {
return new ResultData(
$this->process->getExitCode(),
$e->getMessage(),
$this->getResultData()
);
}
return new ResultData($this->process->getExitCode());
}
/**
*
*/
protected function stop()
{
if ($this->background && isset($this->process) && $this->process->isRunning()) {
$this->process->stop();
$this->printTaskInfo(
"Stopped {command}",
['command' => $this->getCommandDescription()]
);
}
}
/**
* @param array $context
*/
protected function printAction($context = [])
{
$command = $this->getCommandDescription();
$formatted_command = $this->formatCommandDisplay($command);
$dir = $this->workingDirectory ? " in {dir}" : "";
$this->printTaskInfo("Running {command}$dir", [
'command' => $formatted_command,
'dir' => $this->workingDirectory
] + $context);
}
/**
* @param $command
*
* @return mixed
*/
protected function formatCommandDisplay($command)
{
$formatted_command = str_replace("&&", "&&\n", $command);
$formatted_command = str_replace("||", "||\n", $formatted_command);
return $formatted_command;
}
/**
* Gets the data array to be passed to Result().
*
* @return array
* The data array passed to Result().
*/
protected function getResultData()
{
if ($this->isMetadataPrinted) {
return ['time' => $this->getExecutionTime()];
}
return [];
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace Robo\Common;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
trait IO
{
use InputAwareTrait;
use OutputAwareTrait;
/**
* @var \Symfony\Component\Console\Style\SymfonyStyle
*/
protected $io;
/**
* Provide access to SymfonyStyle object.
*
* @return SymfonyStyle
*
* @see http://symfony.com/blog/new-in-symfony-2-8-console-style-guide
*/
protected function io()
{
if (!$this->io) {
$this->io = new SymfonyStyle($this->input(), $this->output());
}
return $this->io;
}
/**
* @param string $nonDecorated
* @param string $decorated
*
* @return string
*/
protected function decorationCharacter($nonDecorated, $decorated)
{
if (!$this->output()->isDecorated() || (strncasecmp(PHP_OS, 'WIN', 3) == 0)) {
return $nonDecorated;
}
return $decorated;
}
/**
* @param string $text
*/
protected function say($text)
{
$char = $this->decorationCharacter('>', '➜');
$this->writeln("$char $text");
}
/**
* @param string $text
* @param int $length
* @param string $color
*/
protected function yell($text, $length = 40, $color = 'green')
{
$char = $this->decorationCharacter(' ', '➜');
$format = "$char <fg=white;bg=$color;options=bold>%s</fg=white;bg=$color;options=bold>";
$this->formattedOutput($text, $length, $format);
}
/**
* @param string $text
* @param int $length
* @param string $format
*/
protected function formattedOutput($text, $length, $format)
{
$lines = explode("\n", trim($text, "\n"));
$maxLineLength = array_reduce(array_map('strlen', $lines), 'max');
$length = max($length, $maxLineLength);
$len = $length + 2;
$space = str_repeat(' ', $len);
$this->writeln(sprintf($format, $space));
foreach ($lines as $line) {
$line = str_pad($line, $length, ' ', STR_PAD_BOTH);
$this->writeln(sprintf($format, " $line "));
}
$this->writeln(sprintf($format, $space));
}
/**
* @param string $question
* @param bool $hideAnswer
*
* @return string
*/
protected function ask($question, $hideAnswer = false)
{
if ($hideAnswer) {
return $this->askHidden($question);
}
return $this->doAsk(new Question($this->formatQuestion($question)));
}
/**
* @param string $question
*
* @return string
*/
protected function askHidden($question)
{
$question = new Question($this->formatQuestion($question));
$question->setHidden(true);
return $this->doAsk($question);
}
/**
* @param string $question
* @param string $default
*
* @return string
*/
protected function askDefault($question, $default)
{
return $this->doAsk(new Question($this->formatQuestion("$question [$default]"), $default));
}
/**
* @param string $question
*
* @return string
*/
protected function confirm($question)
{
return $this->doAsk(new ConfirmationQuestion($this->formatQuestion($question . ' (y/n)'), false));
}
/**
* @param \Symfony\Component\Console\Question\Question $question
*
* @return string
*/
protected function doAsk(Question $question)
{
return $this->getDialog()->ask($this->input(), $this->output(), $question);
}
/**
* @param string $message
*
* @return string
*/
protected function formatQuestion($message)
{
return "<question>? $message</question> ";
}
/**
* @return \Symfony\Component\Console\Helper\QuestionHelper
*/
protected function getDialog()
{
return new QuestionHelper();
}
/**
* @param $text
*/
protected function writeln($text)
{
$this->output()->writeln($text);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Robo\Common;
use Robo\Contract\InflectionInterface;
trait InflectionTrait
{
/**
* Ask the provided parent class to inject all of the dependencies
* that it has and we need.
*
* @param \Robo\Contract\InflectionInterface $parent
*
* @return $this
*/
public function inflect(InflectionInterface $parent)
{
$parent->injectDependencies($this);
return $this;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Robo\Common;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
trait InputAwareTrait
{
/**
* @var \Symfony\Component\Console\Input\InputInterface
*/
protected $input;
/**
* @param \Symfony\Component\Console\Input\InputInterface $input
*
* @return $this
*
* @see \Symfony\Component\Console\Input\InputAwareInterface::setInput()
*/
public function setInput(InputInterface $input)
{
$this->input = $input;
return $this;
}
/**
* @return \Symfony\Component\Console\Input\InputInterface
*/
protected function input()
{
if (!isset($this->input)) {
$this->setInput(new ArgvInput());
}
return $this->input;
}
/**
* Backwards compatibility.
*
* @return \Symfony\Component\Console\Input\InputInterface
*
* @deprecated
*/
protected function getInput()
{
return $this->input();
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Robo\Common;
use Robo\Contract\OutputAdapterInterface;
use Robo\Contract\OutputAwareInterface;
use Robo\Contract\VerbosityThresholdInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Adapt OutputInterface or other output function to the VerbosityThresholdInterface.
*/
class OutputAdapter implements OutputAdapterInterface, OutputAwareInterface
{
use OutputAwareTrait;
protected $verbosityMap = [
VerbosityThresholdInterface::VERBOSITY_NORMAL => OutputInterface::VERBOSITY_NORMAL,
VerbosityThresholdInterface::VERBOSITY_VERBOSE => OutputInterface::VERBOSITY_VERBOSE,
VerbosityThresholdInterface::VERBOSITY_VERY_VERBOSE => OutputInterface::VERBOSITY_VERY_VERBOSE,
VerbosityThresholdInterface::VERBOSITY_DEBUG => OutputInterface::VERBOSITY_DEBUG,
];
public function verbosityMeetsThreshold($verbosityThreshold)
{
if (!isset($this->verbosityMap[$verbosityThreshold])) {
return true;
}
$verbosityThreshold = $this->verbosityMap[$verbosityThreshold];
$verbosity = $this->output()->getVerbosity();
return $verbosity >= $verbosityThreshold;
}
public function writeMessage($message)
{
$this->output()->write($message);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Robo\Common;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
trait OutputAwareTrait
{
/**
* @var \Symfony\Component\Console\Output\OutputInterface
*/
protected $output;
/**
* @param \Symfony\Component\Console\Output\OutputInterface $output
*
* @return $this
*
* @see \Robo\Contract\OutputAwareInterface::setOutput()
*/
public function setOutput(OutputInterface $output)
{
$this->output = $output;
return $this;
}
/**
* @return \Symfony\Component\Console\Output\OutputInterface
*/
protected function output()
{
if (!isset($this->output)) {
$this->setOutput(new NullOutput());
}
return $this->output;
}
/**
* Backwards compatibility
*
* @return \Symfony\Component\Console\Output\OutputInterface
*
* @deprecated
*/
protected function getOutput()
{
return $this->output();
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Robo\Common;
use Psr\Log\LoggerAwareInterface;
use Robo\Contract\ConfigAwareInterface;
use Robo\Contract\OutputAwareInterface;
use Robo\Contract\VerbosityThresholdInterface;
use Symfony\Component\Process\Process;
class ProcessExecutor implements ConfigAwareInterface, LoggerAwareInterface, OutputAwareInterface, VerbosityThresholdInterface
{
use ExecTrait;
use TaskIO; // uses LoggerAwareTrait and ConfigAwareTrait
use ProgressIndicatorAwareTrait;
use OutputAwareTrait;
/**
* @param Process $process
* @return type
*/
public function __construct(Process $process)
{
$this->process = $process;
}
public static function create($container, $process)
{
$processExecutor = new self($process);
$processExecutor->setLogger($container->get('logger'));
$processExecutor->setProgressIndicator($container->get('progressIndicator'));
$processExecutor->setConfig($container->get('config'));
$processExecutor->setOutputAdapter($container->get('outputAdapter'));
return $processExecutor;
}
/**
* @return string
*/
protected function getCommandDescription()
{
return $this->process->getCommandLine();
}
public function run()
{
return $this->execute($this->process);
}
}

View File

@@ -0,0 +1,79 @@
<?php
/*
* This file is derived from part of the Symfony package, which is
* (c) Fabien Potencier <fabien@symfony.com>
*/
namespace Robo\Common;
use Symfony\Component\Process\Exception\InvalidArgumentException;
/**
* ProcessUtils is a bunch of utility methods. We want to allow Robo 1.x
* to work with Symfony 4.x while remaining backwards compatibility. This
* requires us to replace some deprecated functionality removed in Symfony.
*/
class ProcessUtils
{
/**
* This class should not be instantiated.
*/
private function __construct()
{
}
/**
* Escapes a string to be used as a shell argument.
*
* @param string $argument The argument that will be escaped
*
* @return string The escaped argument
*
* @deprecated since version 3.3, to be removed in 4.0. Use a command line array or give env vars to the `Process::start/run()` method instead.
*/
public static function escapeArgument($argument)
{
@trigger_error('The '.__METHOD__.'() method is a copy of a method that was deprecated by Symfony 3.3 and removed in Symfony 4; it will be removed in Robo 2.0.', E_USER_DEPRECATED);
//Fix for PHP bug #43784 escapeshellarg removes % from given string
//Fix for PHP bug #49446 escapeshellarg doesn't work on Windows
//@see https://bugs.php.net/bug.php?id=43784
//@see https://bugs.php.net/bug.php?id=49446
if ('\\' === DIRECTORY_SEPARATOR) {
if ('' === $argument) {
return escapeshellarg($argument);
}
$escapedArgument = '';
$quote = false;
foreach (preg_split('/(")/', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) {
if ('"' === $part) {
$escapedArgument .= '\\"';
} elseif (self::isSurroundedBy($part, '%')) {
// Avoid environment variable expansion
$escapedArgument .= '^%"'.substr($part, 1, -1).'"^%';
} else {
// escape trailing backslash
if ('\\' === substr($part, -1)) {
$part .= '\\';
}
$quote = true;
$escapedArgument .= $part;
}
}
if ($quote) {
$escapedArgument = '"'.$escapedArgument.'"';
}
return $escapedArgument;
}
return "'".str_replace("'", "'\\''", $argument)."'";
}
private static function isSurroundedBy($arg, $char)
{
return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1];
}
}

View File

@@ -0,0 +1,201 @@
<?php
namespace Robo\Common;
/**
* Wrapper around \Symfony\Component\Console\Helper\ProgressBar
*/
class ProgressIndicator
{
use Timer;
/**
* @var \Symfony\Component\Console\Helper\ProgressBar
*/
protected $progressBar;
/**
* @var \Symfony\Component\Console\Output\OutputInterface
*/
protected $output;
/**
* @var bool
*/
protected $progressIndicatorRunning = false;
/**
* @var int
*/
protected $autoDisplayInterval = 0;
/**
* @var int
*/
protected $cachedSteps = 0;
/**
* @var int
*/
protected $totalSteps = 0;
/**
* @var bool
*/
protected $progressBarDisplayed = false;
/**
* @var \Robo\Contract\TaskInterface
*/
protected $owner;
/**
* @param \Symfony\Component\Console\Helper\ProgressBar $progressBar
* @param \Symfony\Component\Console\Output\OutputInterface $output
*/
public function __construct($progressBar, \Symfony\Component\Console\Output\OutputInterface $output)
{
$this->progressBar = $progressBar;
$this->output = $output;
}
/**
* @param int $interval
*/
public function setProgressBarAutoDisplayInterval($interval)
{
if ($this->progressIndicatorRunning) {
return;
}
$this->autoDisplayInterval = $interval;
}
/**
* @return bool
*/
public function hideProgressIndicator()
{
$result = $this->progressBarDisplayed;
if ($this->progressIndicatorRunning && $this->progressBarDisplayed) {
$this->progressBar->clear();
// Hack: progress indicator does not reset cursor to beginning of line on 'clear'
$this->output->write("\x0D");
$this->progressBarDisplayed = false;
}
return $result;
}
public function showProgressIndicator()
{
if ($this->progressIndicatorRunning && !$this->progressBarDisplayed && isset($this->progressBar)) {
$this->progressBar->display();
$this->progressBarDisplayed = true;
$this->advanceProgressIndicatorCachedSteps();
}
}
/**
* @param bool $visible
*/
public function restoreProgressIndicator($visible)
{
if ($visible) {
$this->showProgressIndicator();
}
}
/**
* @param int $totalSteps
* @param \Robo\Contract\TaskInterface $owner
*/
public function startProgressIndicator($totalSteps, $owner)
{
if (!isset($this->progressBar)) {
return;
}
$this->progressIndicatorRunning = true;
if (!isset($this->owner)) {
$this->owner = $owner;
$this->startTimer();
$this->totalSteps = $totalSteps;
$this->autoShowProgressIndicator();
}
}
public function autoShowProgressIndicator()
{
if (($this->autoDisplayInterval < 0) || !isset($this->progressBar) || !$this->output->isDecorated()) {
return;
}
if ($this->autoDisplayInterval <= $this->getExecutionTime()) {
$this->autoDisplayInterval = -1;
$this->progressBar->start($this->totalSteps);
$this->showProgressIndicator();
}
}
/**
* @return bool
*/
public function inProgress()
{
return $this->progressIndicatorRunning;
}
/**
* @param \Robo\Contract\TaskInterface $owner
*/
public function stopProgressIndicator($owner)
{
if ($this->progressIndicatorRunning && ($this->owner === $owner)) {
$this->cleanup();
}
}
protected function cleanup()
{
$this->progressIndicatorRunning = false;
$this->owner = null;
if ($this->progressBarDisplayed) {
$this->progressBar->finish();
// Hack: progress indicator does not always finish cleanly
$this->output->writeln('');
$this->progressBarDisplayed = false;
}
$this->stopTimer();
}
/**
* Erase progress indicator and ensure it never returns. Used
* only during error handlers.
*/
public function disableProgressIndicator()
{
$this->cleanup();
// ProgressIndicator is shared, so this permanently removes
// the program's ability to display progress bars.
$this->progressBar = null;
}
/**
* @param int $steps
*/
public function advanceProgressIndicator($steps = 1)
{
$this->cachedSteps += $steps;
if ($this->progressIndicatorRunning) {
$this->autoShowProgressIndicator();
// We only want to call `advance` if the progress bar is visible,
// because it always displays itself when it is advanced.
if ($this->progressBarDisplayed) {
return $this->advanceProgressIndicatorCachedSteps();
}
}
}
protected function advanceProgressIndicatorCachedSteps()
{
$this->progressBar->advance($this->cachedSteps);
$this->cachedSteps = 0;
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace Robo\Common;
use Robo\Contract\ProgressIndicatorAwareInterface;
use Robo\Contract\VerbosityThresholdInterface;
trait ProgressIndicatorAwareTrait
{
use Timer;
/**
* @var null|\Robo\Common\ProgressIndicator
*/
protected $progressIndicator;
/**
* @return int
*/
public function progressIndicatorSteps()
{
return 0;
}
/**
* @param null|\Robo\Common\ProgressIndicator $progressIndicator
*
* @return ProgressIndicatorAwareInterface
*/
public function setProgressIndicator($progressIndicator)
{
$this->progressIndicator = $progressIndicator;
return $this;
}
/**
* @return null|bool
*/
protected function hideProgressIndicator()
{
if (!$this->progressIndicator) {
return;
}
return $this->progressIndicator->hideProgressIndicator();
}
protected function showProgressIndicator()
{
if (!$this->progressIndicator) {
return;
}
$this->progressIndicator->showProgressIndicator();
}
/**
* @param bool $visible
*/
protected function restoreProgressIndicator($visible)
{
if (!$this->progressIndicator) {
return;
}
$this->progressIndicator->restoreProgressIndicator($visible);
}
/**
* @return int
*/
protected function getTotalExecutionTime()
{
if (!$this->progressIndicator) {
return 0;
}
return $this->progressIndicator->getExecutionTime();
}
protected function startProgressIndicator()
{
$this->startTimer();
if ($this instanceof VerbosityThresholdInterface
&& !$this->verbosityMeetsThreshold()) {
return;
}
if (!$this->progressIndicator) {
return;
}
$totalSteps = $this->progressIndicatorSteps();
$this->progressIndicator->startProgressIndicator($totalSteps, $this);
}
/**
* @return bool
*/
protected function inProgress()
{
if (!$this->progressIndicator) {
return false;
}
return $this->progressIndicator->inProgress();
}
protected function stopProgressIndicator()
{
$this->stopTimer();
if (!$this->progressIndicator) {
return;
}
$this->progressIndicator->stopProgressIndicator($this);
}
protected function disableProgressIndicator()
{
$this->stopTimer();
if (!$this->progressIndicator) {
return;
}
$this->progressIndicator->disableProgressIndicator();
}
protected function detatchProgressIndicator()
{
$this->setProgressIndicator(null);
}
/**
* @param int $steps
*/
protected function advanceProgressIndicator($steps = 1)
{
if (!$this->progressIndicator) {
return;
}
$this->progressIndicator->advanceProgressIndicator($steps);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Robo\Common;
trait ResourceExistenceChecker
{
/**
* Checks if the given input is a file or folder.
*
* @param string|string[] $resources
* @param string $type "file", "dir", "fileAndDir"
*
* @return bool True if no errors were encountered otherwise false.
*/
protected function checkResources($resources, $type = 'fileAndDir')
{
if (!in_array($type, ['file', 'dir', 'fileAndDir'])) {
throw new \InvalidArgumentException(sprintf('Invalid resource check of type "%s" used!', $type));
}
if (is_string($resources)) {
$resources = [$resources];
}
$success = true;
foreach ($resources as $resource) {
$glob = glob($resource);
if ($glob === false) {
$this->printTaskError(sprintf('Invalid glob "%s"!', $resource), $this);
$success = false;
continue;
}
foreach ($glob as $resource) {
if (!$this->checkResource($resource, $type)) {
$success = false;
}
}
}
return $success;
}
/**
* Checks a single resource, file or directory.
*
* It will print an error as well on the console.
*
* @param string $resource File or folder.
* @param string $type "file", "dir", "fileAndDir"
*
* @return bool
*/
protected function checkResource($resource, $type)
{
switch ($type) {
case 'file':
if (!$this->isFile($resource)) {
$this->printTaskError(sprintf('File "%s" does not exist!', $resource), $this);
return false;
}
return true;
case 'dir':
if (!$this->isDir($resource)) {
$this->printTaskError(sprintf('Directory "%s" does not exist!', $resource), $this);
return false;
}
return true;
case 'fileAndDir':
if (!$this->isDir($resource) && !$this->isFile($resource)) {
$this->printTaskError(sprintf('File or directory "%s" does not exist!', $resource), $this);
return false;
}
return true;
}
}
/**
* Convenience method to check the often uses "source => target" file / folder arrays.
*
* @param string|array $resources
*/
protected function checkSourceAndTargetResource($resources)
{
if (is_string($resources)) {
$resources = [$resources];
}
$sources = [];
$targets = [];
foreach ($resources as $source => $target) {
$sources[] = $source;
$target[] = $target;
}
$this->checkResources($sources);
$this->checkResources($targets);
}
/**
* Wrapper method around phps is_dir()
*
* @param string $directory
*
* @return bool
*/
protected function isDir($directory)
{
return is_dir($directory);
}
/**
* Wrapper method around phps file_exists()
*
* @param string $file
*
* @return bool
*/
protected function isFile($file)
{
return file_exists($file);
}
}

View File

@@ -0,0 +1,237 @@
<?php
namespace Robo\Common;
use Robo\Robo;
use Robo\TaskInfo;
use Consolidation\Log\ConsoleLogLevel;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LogLevel;
use Robo\Contract\ProgressIndicatorAwareInterface;
/**
* Task input/output methods. TaskIO is 'used' in BaseTask, so any
* task that extends this class has access to all of the methods here.
* printTaskInfo, printTaskSuccess, and printTaskError are the three
* primary output methods that tasks are encouraged to use. Tasks should
* avoid using the IO trait output methods.
*/
trait TaskIO
{
use LoggerAwareTrait;
use ConfigAwareTrait;
use VerbosityThresholdTrait;
/**
* @return mixed|null|\Psr\Log\LoggerInterface
*/
public function logger()
{
// $this->logger should always be set in Robo core tasks.
if ($this->logger) {
return $this->logger;
}
// TODO: Remove call to Robo::logger() once maintaining backwards
// compatibility with legacy external Robo tasks is no longer desired.
if (!Robo::logger()) {
return null;
}
static $gaveDeprecationWarning = false;
if (!$gaveDeprecationWarning) {
trigger_error('No logger set for ' . get_class($this) . '. Use $this->task(Foo::class) rather than new Foo() in loadTasks to ensure the builder can initialize task the task, or use $this->collectionBuilder()->taskFoo() if creating one task from within another.', E_USER_DEPRECATED);
$gaveDeprecationWarning = true;
}
return Robo::logger();
}
/**
* Print information about a task in progress.
*
* With the Symfony Console logger, NOTICE is displayed at VERBOSITY_VERBOSE
* and INFO is displayed at VERBOSITY_VERY_VERBOSE.
*
* Robo overrides the default such that NOTICE is displayed at
* VERBOSITY_NORMAL and INFO is displayed at VERBOSITY_VERBOSE.
*
* n.b. We should probably have printTaskNotice for our ordinary
* output, and use printTaskInfo for less interesting messages.
*
* @param string $text
* @param null|array $context
*/
protected function printTaskInfo($text, $context = null)
{
// The 'note' style is used for both 'notice' and 'info' log levels;
// However, 'notice' is printed at VERBOSITY_NORMAL, whereas 'info'
// is only printed at VERBOSITY_VERBOSE.
$this->printTaskOutput(LogLevel::NOTICE, $text, $this->getTaskContext($context));
}
/**
* Provide notification that some part of the task succeeded.
*
* With the Symfony Console logger, success messages are remapped to NOTICE,
* and displayed in VERBOSITY_VERBOSE. When used with the Robo logger,
* success messages are displayed at VERBOSITY_NORMAL.
*
* @param string $text
* @param null|array $context
*/
protected function printTaskSuccess($text, $context = null)
{
// Not all loggers will recognize ConsoleLogLevel::SUCCESS.
// We therefore log as LogLevel::NOTICE, and apply a '_level'
// override in the context so that this message will be
// logged as SUCCESS if that log level is recognized.
$context['_level'] = ConsoleLogLevel::SUCCESS;
$this->printTaskOutput(LogLevel::NOTICE, $text, $this->getTaskContext($context));
}
/**
* Provide notification that there is something wrong, but
* execution can continue.
*
* Warning messages are displayed at VERBOSITY_NORMAL.
*
* @param string $text
* @param null|array $context
*/
protected function printTaskWarning($text, $context = null)
{
$this->printTaskOutput(LogLevel::WARNING, $text, $this->getTaskContext($context));
}
/**
* Provide notification that some operation in the task failed,
* and the task cannot continue.
*
* Error messages are displayed at VERBOSITY_NORMAL.
*
* @param string $text
* @param null|array $context
*/
protected function printTaskError($text, $context = null)
{
$this->printTaskOutput(LogLevel::ERROR, $text, $this->getTaskContext($context));
}
/**
* Provide debugging notification. These messages are only
* displayed if the log level is VERBOSITY_DEBUG.
*
* @param string$text
* @param null|array $context
*/
protected function printTaskDebug($text, $context = null)
{
$this->printTaskOutput(LogLevel::DEBUG, $text, $this->getTaskContext($context));
}
/**
* @param string $level
* One of the \Psr\Log\LogLevel constant
* @param string $text
* @param null|array $context
*/
protected function printTaskOutput($level, $text, $context)
{
if (!$this->verbosityMeetsThreshold()) {
return;
}
$logger = $this->logger();
if (!$logger) {
return;
}
// Hide the progress indicator, if it is visible.
$inProgress = $this->hideTaskProgress();
$logger->log($level, $text, $this->getTaskContext($context));
// After we have printed our log message, redraw the progress indicator.
$this->showTaskProgress($inProgress);
}
/**
* @return bool
*/
protected function hideTaskProgress()
{
$inProgress = false;
if ($this instanceof ProgressIndicatorAwareInterface) {
$inProgress = $this->inProgress();
}
// If a progress indicator is running on this task, then we mush
// hide it before we print anything, or its display will be overwritten.
if ($inProgress) {
$inProgress = $this->hideProgressIndicator();
}
return $inProgress;
}
/**
* @param $inProgress
*/
protected function showTaskProgress($inProgress)
{
if ($inProgress) {
$this->restoreProgressIndicator($inProgress);
}
}
/**
* Format a quantity of bytes.
*
* @param int $size
* @param int $precision
*
* @return string
*/
protected function formatBytes($size, $precision = 2)
{
if ($size === 0) {
return 0;
}
$base = log($size, 1024);
$suffixes = array('', 'k', 'M', 'G', 'T');
return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
}
/**
* Get the formatted task name for use in task output.
* This is placed in the task context under 'name', and
* used as the log label by Robo\Common\RoboLogStyle,
* which is inserted at the head of log messages by
* Robo\Common\CustomLogStyle::formatMessage().
*
* @param null|object $task
*
* @return string
*/
protected function getPrintedTaskName($task = null)
{
if (!$task) {
$task = $this;
}
return TaskInfo::formatTaskName($task);
}
/**
* @param null|array $context
*
* @return array with context information
*/
protected function getTaskContext($context = null)
{
if (!$context) {
$context = [];
}
if (!is_array($context)) {
$context = ['task' => $context];
}
if (!array_key_exists('task', $context)) {
$context['task'] = $this;
}
return $context + TaskInfo::getTaskContext($context['task']);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Robo\Common;
class TimeKeeper
{
const MINUTE = 60;
const HOUR = 3600;
const DAY = 86400;
/**
* @var float
*/
protected $startedAt;
/**
* @var float
*/
protected $finishedAt;
public function start()
{
if ($this->startedAt) {
return;
}
// Get time in seconds as a float, accurate to the microsecond.
$this->startedAt = microtime(true);
}
public function stop()
{
$this->finishedAt = microtime(true);
}
/**
* @return float|null
*/
public function elapsed()
{
$finished = $this->finishedAt ? $this->finishedAt : microtime(true);
if ($finished - $this->startedAt <= 0) {
return null;
}
return $finished - $this->startedAt;
}
/**
* Format a duration into a human-readable time
*
* @param float $duration Duration in seconds, with fractional component
*
* @return string
*/
public static function formatDuration($duration)
{
if ($duration >= self::DAY * 2) {
return gmdate('z \d\a\y\s H:i:s', $duration);
}
if ($duration > self::DAY) {
return gmdate('\1 \d\a\y H:i:s', $duration);
}
if ($duration > self::HOUR) {
return gmdate("H:i:s", $duration);
}
if ($duration > self::MINUTE) {
return gmdate("i:s", $duration);
}
return round($duration, 3).'s';
}
}

Some files were not shown because too many files have changed in this diff Show More