Refactored structure

This commit is contained in:
Dmitry Khomutov 2016-04-17 12:34:12 +06:00
commit e5164ae1dd
329 changed files with 277 additions and 457 deletions

145
src/B8Framework/Application.php Executable file
View file

@ -0,0 +1,145 @@
<?php
namespace b8;
use b8\Config;
use b8\Exception\HttpException\NotFoundException;
use b8\Http;
use b8\View;
use b8\Controller;
use b8\Http\Response;
use b8\Http\Request;
class Application
{
/**
* @var array
*/
protected $route;
/**
* @var Controller
*/
protected $controller;
/**
* @var Request
*/
protected $request;
/**
* @var Response
*/
protected $response;
/**
* @var Config
*/
protected $config;
public function __construct(Config $config, Http\Request $request = null)
{
$this->config = $config;
$this->response = new Http\Response();
if (!is_null($request)) {
$this->request = $request;
} else {
$this->request = new Http\Request();
}
$this->router = new Http\Router($this, $this->request, $this->config);
if (method_exists($this, 'init')) {
$this->init();
}
}
public function handleRequest()
{
$this->route = $this->router->dispatch();
if (!empty($this->route['callback'])) {
$callback = $this->route['callback'];
if (!$callback($this->route, $this->response)) {
return $this->response;
}
}
if (!$this->controllerExists($this->route)) {
throw new NotFoundException('Controller ' . $this->toPhpName($this->route['controller']) . ' does not exist!');
}
$action = lcfirst($this->toPhpName($this->route['action']));
if (!$this->getController()->hasAction($action)) {
throw new NotFoundException('Controller ' . $this->toPhpName($this->route['controller']) . ' does not have action ' . $action . '!');
}
return $this->getController()->handleAction($action, $this->route['args']);
}
/**
* @return Controller
*/
public function getController()
{
if (empty($this->controller)) {
$controllerClass = $this->getControllerClass($this->route);
$this->controller = $this->loadController($controllerClass);
}
return $this->controller;
}
/**
* @param string $class
*
* @return Controller
*/
protected function loadController($class)
{
$controller = new $class($this->config, $this->request, $this->response);
$controller->init();
return $controller;
}
/**
* @param array $route
*
* @return bool
*/
protected function controllerExists($route)
{
return class_exists($this->getControllerClass($route));
}
/**
* @param array $route
*
* @return string
*/
protected function getControllerClass($route)
{
$namespace = $this->toPhpName($route['namespace']);
$controller = $this->toPhpName($route['controller']);
return $this->config->get('b8.app.namespace') . '\\' . $namespace . '\\' . $controller . 'Controller';
}
public function isValidRoute($route)
{
if ($this->controllerExists($route)) {
return true;
}
return false;
}
protected function toPhpName($string)
{
$string = str_replace('-', ' ', $string);
$string = ucwords($string);
$string = str_replace(' ', '', $string);
return $string;
}
}

29
src/B8Framework/Cache.php Executable file
View file

@ -0,0 +1,29 @@
<?php
namespace b8;
/**
* @package b8
* @subpackage Cache
*/
class Cache
{
const TYPE_APC = 'ApcCache';
const TYPE_REQUEST = 'RequestCache';
protected static $instance = array();
/**
* Get a cache object of a specified type.
*/
public static function getCache($type = self::TYPE_REQUEST)
{
if (!isset(self::$instance[$type])) {
$class = '\\b8\\Cache\\' . $type;
self::$instance[$type] = new $class();
}
return self::$instance[$type];
}
}

View file

@ -0,0 +1,149 @@
<?php
namespace b8\Cache;
use b8\Type;
class ApcCache implements Type\Cache
{
/**
* Check if caching is enabled.
*
* @return boolean
*/
public function isEnabled()
{
$rtn = false;
$apcCli = ini_get('apc.enable_cli');
if (function_exists('apc_fetch') && (php_sapi_name() != 'cli' || in_array($apcCli, array('1', 1, true, 'On')))) {
$rtn = true;
}
return $rtn;
}
/**
* Get item from the cache:
*
* @param string $key
* @param mixed $default
*
* @return mixed
*/
public function get($key, $default = null)
{
if (!$this->isEnabled()) {
return $default;
}
$success = false;
$rtn = apc_fetch($key, $success);
if (!$success) {
$rtn = $default;
}
return $rtn;
}
/**
* Add an item to the cache:
*
* @param string $key
* @param mixed $value
* @param integer $ttl
*
* @return array|bool
*/
public function set($key, $value = null, $ttl = 0)
{
if (!$this->isEnabled()) {
return false;
}
return apc_store($key, $value, $ttl);
}
/**
* Remove an item from the cache:
*
* @param string $key
*
* @return bool|string[]
*/
public function delete($key)
{
if (!$this->isEnabled()) {
return false;
}
return apc_delete($key);
}
/**
* Check if an item is in the cache:
*
* @param string $key
*
* @return bool|string[]
*/
public function contains($key)
{
if (!$this->isEnabled()) {
return false;
}
return apc_exists($key);
}
/**
* Short-hand syntax for get()
*
* @see Config::get()
*
* @param string $key
*
* @return mixed
*/
public function __get($key)
{
return $this->get($key, null);
}
/**
* Short-hand syntax for set()
*
* @see Config::set()
*
* @param string $key
* @param mixed $value
*
* @return array|bool
*/
public function __set($key, $value = null)
{
return $this->set($key, $value);
}
/**
* Is set
*
* @param string $key
*
* @return bool|string[]
*/
public function __isset($key)
{
return $this->contains($key);
}
/**
* Unset
*
* @param string $key
*/
public function __unset($key)
{
$this->delete($key);
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace b8\Cache;
use b8\Type;
class RequestCache implements Type\Cache
{
protected $data = array();
/**
* Check if caching is enabled.
*/
public function isEnabled()
{
return true;
}
/**
* Get item from the cache:
*/
public function get($key, $default = null)
{
return $this->contains($key) ? $this->data[$key] : $default;
}
/**
* Add an item to the cache:
*/
public function set($key, $value = null, $ttl = 0)
{
$this->data[$key] = $value;
return $this;
}
/**
* Remove an item from the cache:
*/
public function delete($key)
{
if ($this->contains($key)) {
unset($this->data[$key]);
}
return $this;
}
/**
* Check if an item is in the cache:
*/
public function contains($key)
{
return array_key_exists($key, $this->data);
}
/**
* Short-hand syntax for get()
* @see Config::get()
*/
public function __get($key)
{
return $this->get($key, null);
}
/**
* Short-hand syntax for set()
* @see Config::set()
*/
public function __set($key, $value = null)
{
return $this->set($key, $value);
}
/**
* Is set
*/
public function __isset($key)
{
return $this->contains($key);
}
/**
* Unset
*/
public function __unset($key)
{
$this->delete($key);
}
}

176
src/B8Framework/Config.php Executable file
View file

@ -0,0 +1,176 @@
<?php
namespace b8;
use Symfony\Component\Yaml\Parser as YamlParser;
if (!defined('B8_PATH')) {
define('B8_PATH', dirname(__FILE__) . '/');
}
class Config
{
protected static $instance;
public static function getInstance()
{
return self::$instance;
}
/**
* @var array
*/
protected $config = array();
public function __construct($settings = null)
{
self::$instance = $this;
if (empty($settings)) {
return;
} elseif (is_array($settings)) {
// Array of setting data.
$this->setArray($settings);
} elseif (is_string($settings) && file_exists($settings)) {
$this->loadYaml($settings);
}
}
public function loadYaml($yamlFile)
{
// Path to a YAML file.
$parser = new YamlParser();
$yaml = file_get_contents($yamlFile);
$config = (array)$parser->parse($yaml);
if (empty($config)) {
return;
}
$this->setArray($config);
}
/**
* Get a configuration value by key, returning a default value if not set.
* @param $key string
* @param $default mixed
* @return mixed
*/
public function get($key, $default = null)
{
$keyParts = explode('.', $key);
$selected = $this->config;
$i = -1;
$last_part = count($keyParts) - 1;
while ($part = array_shift($keyParts)) {
$i++;
if (!array_key_exists($part, $selected)) {
return $default;
}
if ($i === $last_part) {
return $selected[$part];
} else {
$selected = $selected[$part];
}
}
return $default;
}
/**
* Set a value by key.
* @param $key string
* @param $value mixed
*/
public function set($key, $value = null)
{
$this->config[$key] = $value;
}
/**
* Set an array of values.
*/
public function setArray($array)
{
self::deepMerge($this->config, $array);
}
/**
* Short-hand syntax for get()
* @see Config::get()
*/
public function __get($key)
{
return $this->get($key);
}
/**
* Short-hand syntax for set()
* @see Config::set()
*/
public function __set($key, $value = null)
{
return $this->set($key, $value);
}
/**
* Is set
*/
public function __isset($key)
{
return isset($this->config[$key]);
}
/**
* Unset
*/
public function __unset($key)
{
unset($this->config[$key]);
}
/**
* Deeply merge the $target array onto the $source array.
* The $source array will be modified!
* @param array $source
* @param array $target
*/
public static function deepMerge(&$source, $target)
{
if (count($source) === 0) {
$source = $target;
return;
}
foreach ($target as $target_key => $target_value) {
if (isset($source[$target_key])) {
if (!is_array($source[$target_key]) && !is_array($target_value)) {
// Neither value is an array, overwrite
$source[$target_key] = $target_value;
} elseif (is_array($source[$target_key]) && is_array($target_value)) {
// Both are arrays, deep merge them
self::deepMerge($source[$target_key], $target_value);
} elseif (is_array($source[$target_key])) {
// Source is the array, push target value
$source[$target_key][] = $target_value;
} else {
// Target is the array, push source value and copy back
$target_value[] = $source[$target_key];
$source[$target_key] = $target_value;
}
} else {
// No merge required, just set the value
$source[$target_key] = $target_value;
}
}
}
public function getArray()
{
return $this->config;
}
}

116
src/B8Framework/Controller.php Executable file
View file

@ -0,0 +1,116 @@
<?php
namespace b8;
use b8\Config;
use b8\Http\Request;
use b8\Http\Response;
use b8\View;
/**
* b8 Abstract Controller class
* @package b8
*/
abstract class Controller
{
/**
* @var Request
*/
protected $request;
/**
* @var Response
*/
protected $response;
/**
* @var Config
*/
protected $config;
/**
* @var View
*/
protected $controllerView;
/**
* @var View
*/
protected $view;
public function __construct(Config $config, Request $request, Response $response)
{
$this->config = $config;
$this->request = $request;
$this->response = $response;
}
public function hasAction($name)
{
if (method_exists($this, $name)) {
return true;
}
if (method_exists($this, '__call')) {
return true;
}
return false;
}
/**
* Handles an action on this controller and returns a Response object.
* @return Response
*/
public function handleAction($action, $actionParams)
{
return call_user_func_array(array($this, $action), $actionParams);
}
/**
* Initialise the controller.
*/
abstract public function init();
/**
* Get a hash of incoming request parameters ($_GET, $_POST)
*
* @return array
*/
public function getParams()
{
return $this->request->getParams();
}
/**
* Get a specific incoming request parameter.
*
* @param $key
* @param mixed $default Default return value (if key does not exist)
*
* @return mixed
*/
public function getParam($key, $default = null)
{
return $this->request->getParam($key, $default);
}
/**
* Change the value of an incoming request parameter.
* @param $key
* @param $value
*/
public function setParam($key, $value)
{
return $this->request->setParam($key, $value);
}
/**
* Remove an incoming request parameter.
* @param $key
*/
public function unsetParam($key)
{
return $this->request->unsetParam($key);
}
}

View file

@ -0,0 +1,246 @@
<?php
namespace b8\Controller;
use b8\Controller,
b8\Type\RestUser,
b8\Store\Factory,
b8\Exception\HttpException;
class RestController extends Controller
{
const SEARCHTYPE_AND = 'AND';
const SEARCHTYPE_OR = 'OR';
public $requiresAuthentication = true;
public $updateLastAction = true;
/**
* @var \b8\Type\RestUser
*/
protected $activeUser;
protected $where = array();
protected $limit = null;
protected $offset = null;
protected $joins = array();
protected $arrayDepth = 2;
protected $params = null;
protected $order = array();
protected $group = null;
protected $manualJoins = array();
protected $manualWheres = array();
protected $searchType = self::SEARCHTYPE_AND;
protected $_resourceName = null;
protected $_modelName = null;
protected $_tableName = null;
protected $_modelClass = null;
public function init()
{
}
protected function setControllerView()
{
}
protected function setView()
{
}
public function handleAction($action, $actionParams)
{
$response = call_user_func_array(array($this, $action), $actionParams);
$this->response->setContent($response);
return $this->response;
}
public function setActiveUser(RestUser $user)
{
$this->activeUser = $user;
}
public function getActiveUser()
{
return $this->activeUser;
}
public function index()
{
if(!$this->activeUser->checkPermission('canRead', $this->_resourceName))
{
throw new HttpException\ForbiddenException('You do not have permission do this.');
}
$this->where = $this->_parseWhere();
$this->limit = is_null($this->limit) ? $this->getParam('limit', 25) : $this->limit;
$this->offset = is_null($this->offset) ? $this->getParam('offset', 0) : $this->offset;
$this->order = is_null($this->order) || !count($this->order) ? $this->getParam('order', array()) : $this->order;
$this->group = is_null($this->group) || !count($this->group) ? $this->getParam('group', null) : $this->group;
$this->searchType = $this->getParam('searchType', self::SEARCHTYPE_AND);
$store = Factory::getStore($this->_modelName);
$data = $store->getWhere($this->where, $this->limit, $this->offset, $this->joins, $this->order, $this->manualJoins, $this->group, $this->manualWheres, $this->searchType);
$rtn = array(
'debug' => array(
'where' => $this->where,
'searchType' => $this->searchType,
),
'limit' => $this->limit,
'offset' => $this->offset,
'total' => $data['count'],
'items' => array()
);
foreach($data['items'] as $item)
{
$rtn['items'][] = $item->toArray($this->arrayDepth);
}
return $rtn;
}
/**
*
*/
protected function _parseWhere()
{
$clauses = array(
'fuzzy' => 'like',
'gt' => '>',
'gte' => '>=',
'lt' => '<',
'lte' => '<=',
'neq' => '!=',
'between' => 'between'
);
$where = $this->getParam('where', array());
$where = array_merge($where, $this->where);
if(count($where))
{
foreach($where as &$value)
{
if(!is_array($value) || !isset($value['operator']))
{
if(is_array($value) && count($value) == 1)
{
$value = array_shift($value);
}
$value = array(
'operator' => '=',
'value' => $value
);
}
}
foreach($clauses as $clause => $operator)
{
$fields = $this->getParam($clause, array());
if(count($clause))
{
if(!is_array($fields))
{
$fields = array($fields);
}
foreach($fields as $field)
{
if(isset($where[$field]))
{
$where[$field]['operator'] = $operator;
if($operator == 'like')
{
$where[$field]['value'] = str_replace(' ', '%', $where[$field]['value']);
}
}
}
}
}
}
return $where;
}
public function get($key)
{
if(!$this->activeUser->checkPermission('canRead', $this->_resourceName))
{
throw new HttpException\ForbiddenException('You do not have permission do this.');
}
$rtn = Factory::getStore($this->_modelName)->getByPrimaryKey($key);
if(is_object($rtn) && method_exists($rtn, 'toArray'))
{
$rtn = $rtn->toArray($this->arrayDepth);
}
return array(strtolower($this->_modelName) => $rtn);
}
public function put($key)
{
if(!$this->activeUser->checkPermission('canEdit', $this->_resourceName))
{
throw new HttpException\ForbiddenException('You do not have permission do this.');
}
$store = Factory::getStore($this->_modelName);
if($obj = $store->getByPrimaryKey($key))
{
$obj->setValues($this->getParams());
$rtn = $store->save($obj);
return array(strtolower($this->_modelName) => $rtn->toArray($this->arrayDepth));
}
else
{
return null;
}
}
public function post()
{
if(!$this->activeUser->checkPermission('canCreate', $this->_resourceName))
{
throw new HttpException\ForbiddenException('You do not have permission do this.');
}
$store = Factory::getStore($this->_modelName);
$modelClass = $this->_modelClass;
$obj = new $modelClass();
$obj->setValues($this->getParams());
$rtn = $store->save($obj);
return array(strtolower($this->_modelName) => $rtn->toArray($this->arrayDepth));
}
public function delete($key)
{
if(!$this->activeUser->checkPermission('canDelete', $this->_resourceName))
{
throw new HttpException\ForbiddenException('You do not have permission do this.');
}
$store = Factory::getStore($this->_modelName);
try
{
if($obj = $store->getByPrimaryKey($key))
{
$store->delete($obj);
return array('deleted' => true);
}
}
catch(\Exception $ex)
{
}
return array('deleted' => false);
}
}

151
src/B8Framework/Database.php Executable file
View file

@ -0,0 +1,151 @@
<?php
namespace b8;
class Database extends \PDO
{
protected static $initialised = false;
protected static $servers = array('read' => array(), 'write' => array());
protected static $connections = array('read' => null, 'write' => null);
protected static $details = array();
protected static $lastUsed = array('read' => null, 'write' => null);
/**
* @deprecated
*/
public static function setReadServers($read)
{
$config = Config::getInstance();
$settings = $config->get('b8.database', array());
$settings['servers']['read'] = $read;
$config->set('b8.database', $settings);
}
/**
* @deprecated
*/
public static function setWriteServers($write)
{
$config = Config::getInstance();
$settings = $config->get('b8.database', array());
$settings['servers']['write'] = $write;
$config->set('b8.database', $settings);
}
/**
* @deprecated
*/
public static function setDetails($database, $username, $password)
{
$config = Config::getInstance();
$settings = $config->get('b8.database', array());
$settings['name'] = $database;
$settings['username'] = $username;
$settings['password'] = $password;
$config->set('b8.database', $settings);
}
protected static function init()
{
$config = Config::getInstance();
$settings = $config->get('b8.database', array());
self::$servers['read'] = $settings['servers']['read'];
self::$servers['write'] = $settings['servers']['write'];
self::$details['db'] = $settings['name'];
self::$details['user'] = $settings['username'];
self::$details['pass'] = $settings['password'];
self::$initialised = true;
}
/**
* @param string $type
*
* @return \b8\Database
* @throws \Exception
*/
public static function getConnection($type = 'read')
{
if (!self::$initialised) {
self::init();
}
// If the connection hasn't been used for 5 minutes, force a reconnection:
if (!is_null(self::$lastUsed[$type]) && (time() - self::$lastUsed[$type]) > 300) {
self::$connections[$type] = null;
}
if(is_null(self::$connections[$type])) {
if (is_array(self::$servers[$type])) {
// Shuffle, so we pick a random server:
$servers = self::$servers[$type];
shuffle($servers);
} else {
// Only one server was specified
$servers = array(self::$servers[$type]);
}
$connection = null;
// Loop until we get a working connection:
while(count($servers))
{
// Pull the next server:
$server = array_shift($servers);
if (stristr($server, ':')) {
list($host, $port) = explode(':', $server);
$server = $host . ';port=' . $port;
}
// Try to connect:
try
{
$connection = new self('mysql:host=' . $server . ';dbname=' . self::$details['db'],
self::$details['user'],
self::$details['pass'],
array(
\PDO::ATTR_PERSISTENT => false,
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_TIMEOUT => 2,
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'',
));
}
catch(\PDOException $ex)
{
$connection = false;
}
// Opened a connection? Break the loop:
if($connection)
{
break;
}
}
// No connection? Oh dear.
if(!$connection && $type == 'read')
{
throw new \Exception('Could not connect to any ' . $type . ' servers.');
}
self::$connections[$type] = $connection;
}
self::$lastUsed[$type] = time();
return self::$connections[$type];
}
public function getDetails()
{
return self::$details;
}
public static function reset()
{
self::$connections = array('read' => null, 'write' => null);
self::$lastUsed = array('read' => null, 'write' => null);
self::$initialised = false;
}
}

View file

@ -0,0 +1,153 @@
<?php
namespace b8\Database;
use b8\Database,
b8\Database\Map,
b8\View\Template;
class CodeGenerator
{
protected $_db = null;
protected $_map = null;
protected $_tables = null;
protected $_ns = null;
protected $_path = null;
/**
* @param Database $db
* @param array $namespaces
* @param string $path
* @param bool $includeCountQueries
*/
public function __construct(Database $db, array $namespaces, $path, $includeCountQueries = true)
{
$this->_db = $db;
$this->_ns = $namespaces;
$this->_path = $path;
$this->_map = new Map($this->_db);
$this->_tables = $this->_map->generate();
$this->_counts = $includeCountQueries;
}
protected function getNamespace($modelName)
{
return array_key_exists($modelName, $this->_ns) ? $this->_ns[$modelName] : $this->_ns['default'];
}
public function getPath($namespace)
{
return array_key_exists($namespace, $this->_path) ? $this->_path[$namespace] : $this->_path['default'];
}
public function generateModels()
{
print PHP_EOL . 'GENERATING MODELS' . PHP_EOL . PHP_EOL;
foreach($this->_tables as $tableName => $table)
{
$namespace = $this->getNamespace($table['php_name']);
$modelPath = $this->getPath($namespace) . str_replace('\\', '/', $namespace) . '/Model/';
$basePath = $modelPath . 'Base/';
$modelFile = $modelPath . $table['php_name'] . '.php';
$baseFile = $basePath . $table['php_name'] . 'Base.php';
if (!is_dir($basePath)) {
@mkdir($basePath, 0777, true);
}
$model = $this->_processTemplate($tableName, $table, 'ModelTemplate');
$base = $this->_processTemplate($tableName, $table, 'BaseModelTemplate');
print '-- ' . $table['php_name'] . PHP_EOL;
if(!is_file($modelFile))
{
print '-- -- Writing new Model' . PHP_EOL;
file_put_contents($modelFile, $model);
}
print '-- -- Writing base Model' . PHP_EOL;
file_put_contents($baseFile, $base);
}
}
public function generateStores()
{
print PHP_EOL . 'GENERATING STORES' . PHP_EOL . PHP_EOL;
foreach($this->_tables as $tableName => $table)
{
$namespace = $this->getNamespace($table['php_name']);
$storePath = $this->getPath($namespace) . str_replace('\\', '/', $namespace) . '/Store/';
$basePath = $storePath . 'Base/';
$storeFile = $storePath . $table['php_name'] . 'Store.php';
$baseFile = $basePath . $table['php_name'] . 'StoreBase.php';
if (!is_dir($basePath)) {
@mkdir($basePath, 0777, true);
}
$model = $this->_processTemplate($tableName, $table, 'StoreTemplate');
$base = $this->_processTemplate($tableName, $table, 'BaseStoreTemplate');
print '-- ' . $table['php_name'] . PHP_EOL;
if(!is_file($storeFile))
{
print '-- -- Writing new Store' . PHP_EOL;
file_put_contents($storeFile, $model);
}
print '-- -- Writing base Store' . PHP_EOL;
file_put_contents($baseFile, $base);
}
}
public function generateControllers()
{
print PHP_EOL . 'GENERATING CONTROLLERS' . PHP_EOL . PHP_EOL;
@mkdir($this->_path . 'Controller/Base/', 0777, true);
foreach($this->_tables as $tableName => $table)
{
$namespace = $this->getNamespace($table['php_name']);
$controllerPath = $this->getPath($namespace) . str_replace('\\', '/', $namespace) . '/Controller/';
$basePath = $controllerPath . 'Base/';
$controllerFile = $controllerPath . $table['php_name'] . 'Controller.php';
$baseFile = $basePath . $table['php_name'] . 'ControllerBase.php';
if (!is_dir($basePath)) {
@mkdir($basePath, 0777, true);
}
$model = $this->_processTemplate($tableName, $table, 'ControllerTemplate');
$base = $this->_processTemplate($tableName, $table, 'BaseControllerTemplate');
print '-- ' . $table['php_name'] . PHP_EOL;
if(!is_file($controllerFile))
{
print '-- -- Writing new Controller' . PHP_EOL;
file_put_contents($controllerFile, $model);
}
print '-- -- Writing base Controller' . PHP_EOL;
file_put_contents($baseFile, $base);
}
}
protected function _processTemplate($tableName, $table, $template)
{
$tpl = Template::createFromFile($template, B8_PATH . 'Database/CodeGenerator/');
$tpl->appNamespace = $this->getNamespace($table['php_name']);
$tpl->name = $tableName;
$tpl->table = $table;
$tpl->counts = $this->_counts;
$tpl->addFunction('get_namespace', function($args, $view) {
return $this->getNamespace($view->getVariable($args['model']));
});
return $tpl->render();
}
}

View file

@ -0,0 +1,22 @@
<?php
/**
* {@table.php_name} base controller for table: {@name}
*/
namespace {@appNamespace}\Controller\Base;
use b8\Controller\RestController;
/**
* {@table.php_name} base Controller
* @see {@appNamespace}\Controller\{@table.php_name}
* @uses {@appNamespace}\Store\{@table.php_name}Store
* @uses {@appNamespace}\Model\{@table.php_name}
*/
class {@table.php_name}ControllerBase extends RestController
{
protected $_modelName = '{@table.php_name}';
protected $_resourceName = '{@table.php_name.toLowerCase}s';
protected $_modelClass = '\{@appNamespace}\Model\{@table.php_name}';
}

View file

@ -0,0 +1,268 @@
<?php
/**
* {@table.php_name} base model for table: {@name}
*/
namespace {@appNamespace}\Model\Base;
use {@appNamespace}\Model;
use b8\Store\Factory;
/**
* {@table.php_name} Base Model
*/
class {@table.php_name}Base extends Model
{
/**
* @var array
*/
public static $sleepable = array();
/**
* @var string
*/
protected $tableName = '{@name}';
/**
* @var string
*/
protected $modelName = '{@table.php_name}';
/**
* @var array
*/
protected $data = array(
{loop table.columns}
'{@item.name}' => null,
{/loop}
);
/**
* @var array
*/
protected $getters = array(
// Direct property getters:
{loop table.columns}
'{@item.name}' => 'get{@item.php_name}',
{/loop}
// Foreign key getters:
{loop table.relationships.toOne}
'{@item.php_name}' => 'get{@item.php_name}',
{/loop}
);
/**
* @var array
*/
protected $setters = array(
// Direct property setters:
{loop table.columns}
'{@item.name}' => 'set{@item.php_name}',
{/loop}
// Foreign key setters:
{loop table.relationships.toOne}
'{@item.php_name}' => 'set{@item.php_name}',
{/loop}
);
/**
* @var array
*/
public $columns = array(
{loop table.columns}
'{@item.name}' => array(
'type' => '{@item.type}',
{if item.length}
'length' => {@item.length},
{/if}
{if item.null}
'nullable' => true,
{/if}
{if item.is_primary_key}
'primary_key' => true,
{/if}
{if item.auto}
'auto_increment' => true,
{/if}
{if item.default_is_null}
'default' => null,
{/if}
{ifnot item.default_is_null}
{if item.default}
'default' => {if item.default.isNumeric}{@item.default}{/if}{ifnot item.default.isNumeric}'{@item.default}'{/ifnot},
{/if}
{/ifnot}
),
{/loop}
);
/**
* @var array
*/
public $indexes = array(
{loop table.indexes}
'{@item.name}' => array({if item.unique}'unique' => true, {/if}'columns' => '{@item.columns}'),
{/loop}
);
/**
* @var array
*/
public $foreignKeys = array(
{loop table.relationships.toOne}
'{@item.fk_name}' => array(
'local_col' => '{@item.from_col}',
'update' => '{@item.fk_update}',
'delete' => '{@item.fk_delete}',
'table' => '{@item.table}',
'col' => '{@item.col}'
),
{/loop}
);
{loop table.columns}
/**
* Get the value of {@item.php_name} / {@item.name}.
*
{if item.validate_int}
* @return int
{/if}{if item.validate_string}
* @return string
{/if}{if item.validate_float}
* @return float
{/if}{if item.validate_date}
* @return \DateTime
{/if}
*/
public function get{@item.php_name}()
{
$rtn = $this->data['{@item.name}'];
{if item.validate_date}
if (!empty($rtn)) {
$rtn = new \DateTime($rtn);
}
{/if}
return $rtn;
}
{/loop}
{loop table.columns}
/**
* Set the value of {@item.php_name} / {@item.name}.
*
{if item.validate_null}
* Must not be null.
{/if}{if item.validate_int}
* @param $value int
{/if}{if item.validate_string}
* @param $value string
{/if}{if item.validate_float}
* @param $value float
{/if}{if item.validate_date}
* @param $value \DateTime
{/if}
*/
public function set{@item.php_name}($value)
{
{if item.validate_null}
$this->_validateNotNull('{@item.php_name}', $value);
{/if}
{if item.validate_int}
$this->_validateInt('{@item.php_name}', $value);
{/if}
{if item.validate_string}
$this->_validateString('{@item.php_name}', $value);
{/if}
{if item.validate_float}
$this->_validateFloat('{@item.php_name}', $value);
{/if}
{if item.validate_date}
$this->_validateDate('{@item.php_name}', $value);
{/if}
if ($this->data['{@item.name}'] === $value) {
return;
}
$this->data['{@item.name}'] = $value;
$this->_setModified('{@item.name}');
}
{/loop}{loop table.relationships.toOne}
/**
* Get the {@item.table_php_name} model for this {@parent.table.php_name} by {@item.col_php}.
*
* @uses \{@parent.appNamespace}\Store\{@item.table_php_name}Store::getBy{@item.col_php}()
* @uses \{@parent.appNamespace}\Model\{@item.table_php_name}
* @return \{@parent.appNamespace}\Model\{@item.table_php_name}
*/
public function get{@item.php_name}()
{
$key = $this->get{@item.from_col_php}();
if (empty($key)) {
return null;
}
$cacheKey = 'Cache.{@item.table_php_name}.' . $key;
$rtn = $this->cache->get($cacheKey, null);
if (empty($rtn)) {
$rtn = Factory::getStore('{@item.table_php_name}', '{get_namespace model: item.table_php_name}')->getBy{@item.col_php}($key);
$this->cache->set($cacheKey, $rtn);
}
return $rtn;
}
/**
* Set {@item.php_name} - Accepts an ID, an array representing a {@item.table_php_name} or a {@item.table_php_name} model.
*
* @param $value mixed
*/
public function set{@item.php_name}($value)
{
// Is this an instance of {@item.table_php_name}?
if ($value instanceof \{@parent.appNamespace}\Model\{@item.table_php_name}) {
return $this->set{@item.php_name}Object($value);
}
// Is this an array representing a {@item.table_php_name} item?
if (is_array($value) && !empty($value['{@item.col}'])) {
return $this->set{@item.from_col_php}($value['{@item.col}']);
}
// Is this a scalar value representing the ID of this foreign key?
return $this->set{@item.from_col_php}($value);
}
/**
* Set {@item.php_name} - Accepts a {@item.table_php_name} model.
*
* @param $value \{@parent.appNamespace}\Model\{@item.table_php_name}
*/
public function set{@item.php_name}Object(\{@parent.appNamespace}\Model\{@item.table_php_name} $value)
{
return $this->set{@item.from_col_php}($value->get{@item.col_php}());
}
{/loop}{loop table.relationships.toMany}
/**
* Get {@item.table_php} models by {@item.from_col_php} for this {@parent.table.php_name}.
*
* @uses \{@parent.appNamespace}\Store\{@item.table_php}Store::getBy{@item.from_col_php}()
* @uses \{@parent.appNamespace}\Model\{@item.table_php}
* @return \{@parent.appNamespace}\Model\{@item.table_php}[]
*/
public function get{@item.php_name}()
{
return Factory::getStore('{@item.table_php}', '{get_namespace model: item.table_php_name}')->getBy{@item.from_col_php}($this->get{@item.col_php}());
}
{/loop}}

View file

@ -0,0 +1,101 @@
<?php
/**
* {@table.php_name} base store for table: {@name}
*/
namespace {@appNamespace}\Store\Base;
use b8\Database;
use b8\Exception\HttpException;
use {@appNamespace}\Store;
use {@appNamespace}\Model\{@table.php_name};
/**
* {@table.php_name} Base Store
*/
class {@table.php_name}StoreBase extends Store
{
protected $tableName = '{@name}';
protected $modelName = '\{@appNamespace}\Model\{@table.php_name}';
{if table.primary_key}
protected $primaryKey = '{@table.primary_key.column}';
public function getByPrimaryKey($value, $useConnection = 'read')
{
return $this->getBy{@table.primary_key.php_name}($value, $useConnection);
}
{/if}
{ifnot table.primary_key}
public function getByPrimaryKey($value, $useConnection = 'read')
{
throw new \Exception('getByPrimaryKey is not implemented for this store, as the table has no primary key.');
}
{/ifnot}
{loop table.columns}
{if item.unique_indexed}
public function getBy{@item.php_name}($value, $useConnection = 'read')
{
if (is_null($value)) {
throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.');
}
$query = 'SELECT * FROM `{@parent.name}` WHERE `{@item.name}` = :{@item.name} LIMIT 1';
$stmt = Database::getConnection($useConnection)->prepare($query);
$stmt->bindValue(':{@item.name}', $value);
if ($stmt->execute()) {
if ($data = $stmt->fetch(\PDO::FETCH_ASSOC)) {
return new {@parent.table.php_name}($data);
}
}
return null;
}
{/if}
{if item.many_indexed}
public function getBy{@item.php_name}($value, $limit = 1000, $useConnection = 'read')
{
if (is_null($value)) {
throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.');
}
{if counts}
$query = 'SELECT COUNT(*) AS cnt FROM `{@parent.name}` WHERE `{@item.name}` = :{@item.name}';
$stmt = Database::getConnection($useConnection)->prepare($query);
$stmt->bindValue(':{@item.name}', $value);
if ($stmt->execute()) {
$res = $stmt->fetch(\PDO::FETCH_ASSOC);
$count = (int)$res['cnt'];
} else {
$count = 0;
}
{/if}
$query = 'SELECT * FROM `{@parent.name}` WHERE `{@item.name}` = :{@item.name} LIMIT :limit';
$stmt = Database::getConnection($useConnection)->prepare($query);
$stmt->bindValue(':{@item.name}', $value);
$stmt->bindValue(':limit', (int)$limit, \PDO::PARAM_INT);
if ($stmt->execute()) {
$res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$map = function ($item) {
return new {@parent.table.php_name}($item);
};
$rtn = array_map($map, $res);
{ifnot counts}$count = count($rtn);{/ifnot}
return array('items' => $rtn, 'count' => $count);
} else {
return array('items' => array(), 'count' => 0);
}
}
{/if}
{/loop}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* {@table.php_name} controller for table: {@name}
*/
namespace {@appNamespace}\Controller;
use {@appNamespace}\Controller\Base\{@table.php_name}ControllerBase;
/**
* {@table.php_name} Controller
* @uses {@appNamespace}\Controller\Base\{@table.php_name}Base
* @uses {@appNamespace}\Store\{@table.php_name}Store
* @uses {@appNamespace}\Model\{@table.php_name}
*/
class {@table.php_name}Controller extends {@table.php_name}ControllerBase
{
// This class has been left blank so that you can modify it - changes in this file will not be overwritten.
}

View file

@ -0,0 +1,18 @@
<?php
/**
* {@table.php_name} model for table: {@name}
*/
namespace {@appNamespace}\Model;
use {@appNamespace}\Model\Base\{@table.php_name}Base;
/**
* {@table.php_name} Model
* @uses {@appNamespace}\Model\Base\{@table.php_name}Base
*/
class {@table.php_name} extends {@table.php_name}Base
{
// This class has been left blank so that you can modify it - changes in this file will not be overwritten.
}

View file

@ -0,0 +1,18 @@
<?php
/**
* {@table.php_name} store for table: {@name}
*/
namespace {@appNamespace}\Store;
use {@appNamespace}\Store\Base\{@table.php_name}StoreBase;
/**
* {@table.php_name} Store
* @uses {@appNamespace}\Store\Base\{@table.php_name}StoreBase
*/
class {@table.php_name}Store extends {@table.php_name}StoreBase
{
// This class has been left blank so that you can modify it - changes in this file will not be overwritten.
}

View file

@ -0,0 +1,428 @@
<?php
/**
* Database generator updates a database to match a set of Models.
*/
namespace b8\Database;
use b8\Database;
class Generator
{
protected $_db = null;
protected $_map = null;
protected $_tables = null;
protected $_ns = null;
protected $_path = null;
public function __construct(Database $db, $namespace, $path)
{
$this->_db = $db;
$this->_ns = $namespace;
$this->_path = $path;
$this->_map = new Map($this->_db);
$this->_tables = $this->_map->generate();
}
public function generate()
{
error_reporting(E_ERROR & E_WARNING);
$di = new \DirectoryIterator($this->_path);
$this->_todo = array(
'drop_fk' => array(),
'drop_index'=> array(),
'create' => array(),
'alter' => array(),
'add_index' => array(),
'add_fk' => array(),
);
foreach($di as $file)
{
if($file->isDot())
{
continue;
}
$fileName = explode('.', $file->getBasename());
if ($fileName[count($fileName)-1] != 'php')
{
continue;
}
$modelName = '\\' . $this->_ns . '\\Model\\Base\\' . str_replace('.php', '', $file->getFilename());
require_once($this->_path . $file->getFilename());
$model = new $modelName();
$columns = $model->columns;
$indexes = $model->indexes;
$foreignKeys = $model->foreignKeys;
$tableName = $model->getTableName();
if(!array_key_exists($tableName, $this->_tables))
{
$this->_createTable($tableName, $columns, $indexes, $foreignKeys);
continue;
}
else
{
$table = $this->_tables[$tableName];
$this->_updateColumns($tableName, $table, $columns);
$this->_updateRelationships($tableName, $table, $foreignKeys);
$this->_updateIndexes($tableName, $table, $indexes);
}
}
print 'DROP FK: ' . count($this->_todo['drop_fk']) . PHP_EOL;
print 'DROP INDEX: ' . count($this->_todo['drop_index']) . PHP_EOL;
print 'CREATE TABLE: ' . count($this->_todo['create']) . PHP_EOL;
print 'ALTER TABLE: ' . count($this->_todo['alter']) . PHP_EOL;
print 'ADD INDEX: ' . count($this->_todo['add_index']) . PHP_EOL;
print 'ADD FK: ' . count($this->_todo['add_fk']) . PHP_EOL;
$order = array_keys($this->_todo);
while($group = array_shift($order))
{
if(!isset($this->_todo[$group]) || !is_array($this->_todo[$group]) || !count($this->_todo[$group]))
{
continue;
}
foreach($this->_todo[$group] as $query)
{
try
{
//print $query . PHP_EOL;
$this->_db->query($query);
}
catch(\Exception $ex)
{
print 'FAILED TO EXECUTE: ' . $query . PHP_EOL;
print $ex->getMessage().PHP_EOL.PHP_EOL;
}
}
}
}
protected function _createTable($tbl, $cols, $idxs, $fks)
{
$defs = array();
$pks = array();
foreach($cols as $colName => $def)
{
$add = '`' . $colName . '` ' . $def['type'];
switch($def['type'])
{
case 'text':
case 'longtext':
case 'mediumtext':
case 'date':
case 'datetime':
case 'float':
$add .= '';
break;
default:
$add .= !empty($def['length']) ? '(' . $def['length'] . ')' : '';
break;
}
if(empty($def['nullable']) || !$def['nullable'])
{
$add .= ' NOT NULL ';
}
if(!empty($def['default']))
{
$add .= ' DEFAULT ' . (is_numeric($def['default']) ? $def['default'] : '\'' . $def['default'] . '\'');
}
if(!empty($def['auto_increment']) && $def['auto_increment'])
{
$add .= ' AUTO_INCREMENT ';
}
if(!empty($def['primary_key']) && $def['primary_key'])
{
$pks[] = '`' . $colName . '`';
}
$defs[] = $add;
}
if(count($pks))
{
$defs[] = 'PRIMARY KEY (' . implode(', ', $pks) . ')';
}
$stmt = 'CREATE TABLE `' . $tbl . '` (' . PHP_EOL;
$stmt .= implode(", \n", $defs);
$stmt .= PHP_EOL . ') ENGINE=InnoDB DEFAULT CHARSET=utf8';
$stmt .= PHP_EOL;
$this->_todo['create'][] = $stmt;
foreach($idxs as $name => $idx)
{
$this->_addIndex($tbl, $name, $idx);
}
foreach($fks as $name => $fk)
{
$this->_addFk($tbl, $name, $fk);
}
}
protected function _updateColumns($tableName, $table, $columns)
{
$currentColumns = $table['columns'];
while($column = array_shift($currentColumns))
{
if(!array_key_exists($column['name'], $columns))
{
$this->_todo['alter'][$tableName.'.'.$column['name']] = 'ALTER TABLE `' . $tableName . '` DROP COLUMN `' . $column['name'] . '`';
}
else
{
$model = $columns[$column['name']];
$model['nullable'] = !isset($model['nullable']) ? false : $model['nullable'];
$model['default'] = !isset($model['default']) ? false : $model['default'];
$model['auto_increment'] = !isset($model['auto_increment']) ? false : $model['auto_increment'];
$model['primary_key'] = !isset($model['primary_key']) ? false : $model['primary_key'];
$column['is_primary_key'] = !isset($column['is_primary_key']) ? false : $column['is_primary_key'];
if( $column['type'] != $model['type'] ||
($column['length'] != $model['length'] && !in_array($model['type'], array('text', 'longtext', 'mediumtext', 'date', 'datetime', 'float'))) ||
$column['null'] != $model['nullable'] ||
$column['default'] != $model['default'] ||
$column['auto'] != $model['auto_increment'])
{
$this->_updateColumn($tableName, $column['name'], $column['name'], $model);
}
}
unset($columns[$column['name']]);
}
if(count($columns))
{
foreach($columns as $name => $model)
{
// Check if we're renaming a column:
if(isset($model['rename']))
{
unset($this->_todo['alter'][$tableName.'.'.$model['rename']]);
$this->_updateColumn($tableName, $model['rename'], $name, $model);
continue;
}
// New column
$add = '`' . $name . '` ' . $model['type'];;
switch($model['type'])
{
case 'text':
case 'longtext':
case 'mediumtext':
case 'date':
case 'datetime':
case 'float':
$add .= '';
break;
default:
$add .= !empty($model['length']) ? '(' . $model['length'] . ')' : '';
break;
}
if(empty($model['nullable']) || !$model['nullable'])
{
$add .= ' NOT NULL ';
}
if(!empty($model['default']))
{
$add .= ' DEFAULT ' . (is_numeric($model['default']) ? $model['default'] : '\'' . $model['default'] . '\'');
}
if(!empty($model['auto_increment']) && $model['auto_increment'])
{
$add .= ' AUTO_INCREMENT ';
}
if(!empty($model['primary_key']) && $model['primary_key'] && !isset($table['indexes']['PRIMARY']))
{
$add .= ' PRIMARY KEY ';
}
$this->_todo['alter'][] = 'ALTER TABLE `' . $tableName . '` ADD COLUMN ' . $add;
}
}
}
protected function _updateColumn($tableName, $prevName, $newName, $model)
{
$add = '`' . $newName . '` ' . $model['type'];;
switch($model['type'])
{
case 'text':
case 'longtext':
case 'mediumtext':
case 'date':
case 'datetime':
case 'float':
$add .= '';
break;
default:
$add .= !empty($model['length']) ? '(' . $model['length'] . ')' : '';
break;
}
if(empty($model['nullable']) || !$model['nullable'])
{
$add .= ' NOT NULL ';
}
if(!empty($model['default']))
{
$add .= ' DEFAULT ' . (is_numeric($model['default']) ? $model['default'] : '\'' . $model['default'] . '\'');
}
if(!empty($model['auto_increment']) && $model['auto_increment'])
{
$add .= ' AUTO_INCREMENT ';
}
$this->_todo['alter'][] = 'ALTER TABLE `' . $tableName . '` CHANGE COLUMN `' . $prevName . '` ' . $add;
}
protected function _updateRelationships($tableName, $table, $foreignKeys)
{
$current = $table['relationships']['toOne'];
while($foreignKey = array_shift($current))
{
if(!array_key_exists($foreignKey['fk_name'], $foreignKeys))
{
$this->_dropFk($tableName, $foreignKey['fk_name']);
}
elseif( $foreignKey['from_col'] != $foreignKeys[$foreignKey['fk_name']]['local_col'] ||
$foreignKey['table'] != $foreignKeys[$foreignKey['fk_name']]['table'] ||
$foreignKey['col'] != $foreignKeys[$foreignKey['fk_name']]['col'] ||
$foreignKey['fk_update'] != $foreignKeys[$foreignKey['fk_name']]['update'] ||
$foreignKey['fk_delete'] != $foreignKeys[$foreignKey['fk_name']]['delete'])
{
$this->_alterFk($tableName, $foreignKey['fk_name'], $foreignKeys[$foreignKey['fk_name']]);
}
unset($foreignKeys[$foreignKey['fk_name']]);
}
if(count($foreignKeys))
{
foreach($foreignKeys as $name => $foreignKey)
{
// New column
$this->_addFk($tableName, $name, $foreignKey);
}
}
}
protected function _updateIndexes($tableName, $table, $indexes)
{
$current = $table['indexes'];
while($index = array_shift($current))
{
if(!array_key_exists($index['name'], $indexes))
{
$this->_dropIndex($tableName, $index['name']);
}
elseif( $index['unique'] != $indexes[$index['name']]['unique'] ||
$index['columns'] != $indexes[$index['name']]['columns'])
{
$this->_alterIndex($tableName, $index['name'], $index);
}
unset($indexes[$index['name']]);
}
if(count($indexes))
{
foreach($indexes as $name => $index)
{
if($name == 'PRIMARY')
{
continue;
}
// New index
$this->_addIndex($tableName, $name, $index);
}
}
}
protected function _addIndex($table, $name, $idx, $stage = 'add_index')
{
if($name == 'PRIMARY')
{
return;
}
$q = 'CREATE ' . (isset($idx['unique']) && $idx['unique'] ? 'UNIQUE' : '') . ' INDEX `' . $name . '` ON `' . $table . '` (' . $idx['columns'] . ')';
$this->_todo[$stage][] = $q;
}
protected function _alterIndex($table, $name, $idx, $stage = 'index')
{
$this->_dropIndex($table, $name, $stage);
$this->_addIndex($table, $name, $idx, $stage);
}
protected function _dropIndex($table, $idxName, $stage = 'drop_index')
{
if($idxName == 'PRIMARY')
{
return;
}
$q = 'DROP INDEX `' . $idxName . '` ON `' . $table . '`';
$this->_todo[$stage][] = $q;
}
protected function _addFk($table, $name, $fk)
{
$q = 'ALTER TABLE `' . $table . '` ADD CONSTRAINT `' . $name . '` FOREIGN KEY (`' . $fk['local_col'] . '`) REFERENCES `'.$fk['table'].'` (`'.$fk['col'].'`)';
if(!empty($fk['delete']))
{
$q .= ' ON DELETE ' . $fk['delete'] . ' ';
}
if(!empty($fk['update']))
{
$q .= ' ON UPDATE ' . $fk['update'] . ' ';
}
$this->_todo['add_fk'][] = $q;
}
protected function _alterFk($table, $name, $fk)
{
$this->_dropFk($table, $name);
$this->_addFk($table, $name, $fk);
}
protected function _dropFk($table, $name)
{
$q = 'ALTER TABLE `'.$table.'` DROP FOREIGN KEY `' . $name . '`';
$this->_todo['drop_fk'][] = $q;
}
}

258
src/B8Framework/Database/Map.php Executable file
View file

@ -0,0 +1,258 @@
<?php
namespace b8\Database;
use b8\Database;
class Map
{
protected $_db = null;
protected $_tables = array();
public function __construct(Database $db)
{
$this->_db = $db;
}
public function generate()
{
$tables = $this->_getTables();
foreach($tables as $table)
{
$this->_tables[$table] = array();
$this->_tables[$table]['php_name'] = $this->_generatePhpName($table);
}
$this->_getRelationships();
$this->_getColumns();
$this->_getIndexes();
return $this->_tables;
}
protected function _getTables()
{
$details = $this->_db->getDetails();
$rtn = array();
foreach($this->_db->query('SHOW TABLES')->fetchAll(\PDO::FETCH_ASSOC) as $tbl)
{
$rtn[] = $tbl['Tables_in_' . $details['db']];
}
return $rtn;
}
protected function _getRelationships()
{
foreach($this->_tables as $table => $t)
{
$res = $this->_db->query('SHOW CREATE TABLE `'.$table.'`')->fetchAll(\PDO::FETCH_ASSOC);
foreach($res as $r)
{
$str = $r['Create Table'];
$matches = array();
if(preg_match_all('/CONSTRAINT\s+\`([a-zA-Z0-9_]+)\`\s+FOREIGN\s+KEY\s+\(\`([a-zA-Z0-9_]+)\`\)\s+REFERENCES\s+\`([a-zA-Z0-9_]+)\`\s+\(\`([a-zA-Z0-9_]+)\`\)(\s+ON (DELETE|UPDATE) (SET NULL|NO ACTION|CASCADE|RESTRICT))?(\s+ON (DELETE|UPDATE) (SET NULL|NO ACTION|CASCADE|RESTRICT))?/', $str, $matches))
{
for($i = 0; $i < count($matches[0]); $i++)
{
$fromTable = $table;
$fromCol = $matches[2][$i];
$toTable = $matches[3][$i];
$toCol = $matches[4][$i];
$fkName = $matches[1][$i];
$fk = array();
if(isset($matches[6][$i]))
{
$fk[$matches[6][$i]] = $matches[7][$i];
}
if(isset($matches[9][$i]))
{
$fk[$matches[9][$i]] = $matches[10][$i];
}
$fk['UPDATE'] = empty($fk['UPDATE']) ? '' : $fk['UPDATE'];
$fk['DELETE'] = empty($fk['DELETE']) ? '' : $fk['DELETE'];
if(isset($this->_tables[$fromTable]) && isset($this->_tables[$toTable]))
{
$phpName = $this->_generateFkName($fromCol, $this->_tables[$fromTable]['php_name']);
$this->_tables[$fromTable]['relationships']['toOne'][$fromCol] = array('fk_name' => $fkName, 'fk_delete' => $fk['DELETE'], 'fk_update' => $fk['UPDATE'], 'table_php_name' => $this->_tables[$toTable]['php_name'], 'from_col_php' => $this->_generatePhpName($fromCol), 'from_col' => $fromCol, 'php_name' => $phpName, 'table' => $toTable, 'col' => $toCol, 'col_php' => $this->_generatePhpName($toCol));
$phpName = $this->_generateFkName($fromCol, $this->_tables[$fromTable]['php_name']) . $this->_tables[$fromTable]['php_name'].'s';
$this->_tables[$toTable]['relationships']['toMany'][] = array('from_col_php' => $this->_generatePhpName($fromCol), 'php_name' => $phpName, 'thisCol' => $toCol, 'table' => $fromTable, 'table_php' => $this->_generatePhpName($fromTable), 'fromCol' => $fromCol, 'col_php' => $this->_generatePhpName($toCol));
}
}
}
}
}
}
protected function _getColumns()
{
foreach($this->_tables as $key => &$val)
{
$cols = array();
foreach($this->_db->query('DESCRIBE `' . $key . '`')->fetchAll(\PDO::FETCH_ASSOC) as $column)
{
$col = $this->_processColumn(array(), $column, $val);
$cols[$col['name']] = $col;
}
$val['columns'] = $cols;
}
}
protected function _getIndexes()
{
foreach($this->_tables as $key => &$val)
{
$indexes = array();
foreach($this->_db->query('SHOW INDEXES FROM `' . $key . '`')->fetchAll(\PDO::FETCH_ASSOC) as $idx)
{
if(!isset($indexes[$idx['Key_name']]))
{
$indexes[$idx['Key_name']] = array();
$indexes[$idx['Key_name']]['name'] = $idx['Key_name'];
$indexes[$idx['Key_name']]['unique'] = ($idx['Non_unique'] == '0') ? true : false;
$indexes[$idx['Key_name']]['columns'] = array();
}
$indexes[$idx['Key_name']]['columns'][$idx['Seq_in_index']] = $idx['Column_name'];
}
$indexes = array_map(function($idx)
{
ksort($idx['columns']);
$idx['columns'] = implode(', ', $idx['columns']);
return $idx;
}, $indexes);
$val['indexes'] = $indexes;
}
}
protected function _processColumn($col, $column, &$table)
{
$col['name'] = $column['Field'];
$col['php_name']= $this->_generatePhpName($col['name']);
$matches = array();
preg_match('/^([a-zA-Z]+)(\()?([0-9\,]+)?(\))?/', $column['Type'], $matches);
$col['type'] = strtolower($matches[1]);
if(isset($matches[3]))
{
$col['length'] = $matches[3];
}
$col['null'] = strtolower($column['Null']) == 'yes' ? true : false;
$col['auto'] = strtolower($column['Extra']) == 'auto_increment' ? true : false;
if ($column['Default'] == 'NULL' || is_null($column['Default'])) {
$col['default_is_null'] = true;
} else {
$col['default_is_null'] = false;
$col['default'] = $column['Default'];
}
if(!empty($column['Key']))
{
if($column['Key'] == 'PRI')
{
$col['is_primary_key'] = true;
$table['primary_key'] = array('column' => $col['name'], 'php_name' => $col['php_name']);
}
if($column['Key'] == 'PRI' || $column['Key'] == 'UNI')
{
$col['unique_indexed'] = true;
}
else
{
$col['many_indexed'] = true;
}
}
$col['validate']= array();
if(!$col['null'])
{
$col['validate_null'] = true;
}
switch($col['type'])
{
case 'tinyint':
case 'smallint':
case 'int':
case 'mediumint':
case 'bigint':
$col['php_type'] = 'int';
$col['to_php'] = '_sqlToInt';
$col['validate_int']= true;
break;
case 'float':
case 'decimal':
$col['php_type'] = 'float';
$col['to_php'] = '_sqlToFloat';
$col['validate_float'] = true;
break;
case 'datetime':
case 'date':
$col['php_type'] = 'DateTime';
$col['to_php'] = '_sqlToDateTime';
$col['to_sql'] = '_dateTimeToSql';
$col['validate_date'] = true;
break;
case 'varchar':
case 'text':
default:
$col['php_type'] = 'string';
$col['validate_string'] = true;
break;
}
return $col;
}
protected function _generatePhpName($sqlName)
{
$rtn = $sqlName;
$rtn = str_replace('_', ' ', $rtn);
$rtn = ucwords($rtn);
$rtn = str_replace(' ', '', $rtn);
return $rtn;
}
protected function _generateFkName($sqlName, $tablePhpName)
{
$fkMethod = substr($sqlName, 0, strripos($sqlName, '_'));
if(empty($fkMethod))
{
$fkMethod = (substr(strtolower($sqlName), -2) == 'id') ? substr($sqlName, 0, -2) : $tablePhpName;
}
$fkMethod = str_replace('_', ' ', $fkMethod);
$fkMethod = ucwords($fkMethod);
$fkMethod = str_replace(' ', '', $fkMethod);
return $fkMethod;
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace b8\Exception;
class HttpException extends \Exception
{
protected $errorCode = 500;
protected $statusMessage = 'Internal Server Error';
public function getErrorCode()
{
return $this->errorCode;
}
public function getStatusMessage()
{
return $this->statusMessage;
}
public function getHttpHeader()
{
return 'HTTP/1.1 ' . $this->errorCode . ' ' . $this->statusMessage;
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace b8\Exception\HttpException;
use b8\Exception\HttpException;
class BadRequestException extends HttpException
{
protected $errorCode = 400;
protected $statusMessage = 'Bad Request';
}

View file

@ -0,0 +1,10 @@
<?php
namespace b8\Exception\HttpException;
use b8\Exception\HttpException;
class ForbiddenException extends HttpException
{
protected $errorCode = 403;
protected $statusMessage = 'Forbidden';
}

View file

@ -0,0 +1,10 @@
<?php
namespace b8\Exception\HttpException;
use b8\Exception\HttpException;
class NotAuthorizedException extends HttpException
{
protected $errorCode = 401;
protected $statusMessage = 'Not Authorized';
}

View file

@ -0,0 +1,10 @@
<?php
namespace b8\Exception\HttpException;
use b8\Exception\HttpException;
class NotFoundException extends HttpException
{
protected $errorCode = 404;
protected $statusMessage = 'Not Found';
}

View file

@ -0,0 +1,8 @@
<?php
namespace b8\Exception\HttpException;
use b8\Exception\HttpException;
class ServerErrorException extends HttpException
{
}

View file

@ -0,0 +1,10 @@
<?php
namespace b8\Exception\HttpException;
use b8\Exception\HttpException;
class ValidationException extends HttpException
{
protected $errorCode = 400;
protected $statusMessage = 'Bad Request';
}

44
src/B8Framework/Form.php Executable file
View file

@ -0,0 +1,44 @@
<?php
namespace b8;
use b8\Form\FieldSet,
b8\View;
class Form extends FieldSet
{
protected $_action = '';
protected $_method = 'POST';
public function getAction()
{
return $this->_action;
}
public function setAction($action)
{
$this->_action = $action;
}
public function getMethod()
{
return $this->_method;
}
public function setMethod($method)
{
$this->_method = $method;
}
protected function _onPreRender(View &$view)
{
$view->action = $this->getAction();
$view->method = $this->getMethod();
parent::_onPreRender($view);
}
public function __toString()
{
return $this->render();
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace b8\Form;
class ControlGroup extends FieldSet
{
}

118
src/B8Framework/Form/Element.php Executable file
View file

@ -0,0 +1,118 @@
<?php
namespace b8\Form;
use b8\View;
use b8\Config;
abstract class Element
{
protected $_name;
protected $_id;
protected $_label;
protected $_css;
protected $_ccss;
protected $_parent;
public function __construct($name = null)
{
if(!is_null($name))
{
$this->setName($name);
}
}
public function getName()
{
return $this->_name;
}
public function setName($name)
{
$this->_name = strtolower(preg_replace('/([^a-zA-Z0-9_\-])/', '', $name));
return $this;
}
public function getId()
{
return !$this->_id ? 'element-'.$this->_name : $this->_id;
}
public function setId($id)
{
$this->_id = $id;
return $this;
}
public function getLabel()
{
return $this->_label;
}
public function setLabel($label)
{
$this->_label = $label;
return $this;
}
public function getClass()
{
return $this->_css;
}
public function setClass($class)
{
$this->_css = $class;
return $this;
}
public function getContainerClass()
{
return $this->_ccss;
}
public function setContainerClass($class)
{
$this->_ccss = $class;
return $this;
}
public function setParent(Element $parent)
{
$this->_parent = $parent;
return $this;
}
public function render($viewFile = null)
{
$viewPath = Config::getInstance()->get('b8.view.path');
if(is_null($viewFile))
{
$class = explode('\\', get_called_class());
$viewFile = end($class);
}
if(file_exists($viewPath . 'Form/' . $viewFile . '.phtml'))
{
$view = new View('Form/' . $viewFile);
}
else
{
$view = new View($viewFile, B8_PATH . 'Form/View/');
}
$view->name = $this->getName();
$view->id = $this->getId();
$view->label = $this->getLabel();
$view->css = $this->getClass();
$view->ccss = $this->getContainerClass();
$view->parent = $this->_parent;
$this->_onPreRender($view);
return $view->render();
}
abstract protected function _onPreRender(View &$view);
}

View file

@ -0,0 +1,19 @@
<?php
namespace b8\Form\Element;
use b8\Form\Input,
b8\View;
class Button extends Input
{
public function validate()
{
return true;
}
protected function _onPreRender(View &$view)
{
parent::_onPreRender($view);
$view->type = 'button';
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace b8\Form\Element;
use b8\View,
b8\Form\Input;
class Checkbox extends Input
{
protected $_checked;
protected $_checkedValue;
public function getCheckedValue()
{
return $this->_checkedValue;
}
public function setCheckedValue($value)
{
$this->_checkedValue = $value;
}
public function setValue($value)
{
if(is_bool($value) && $value == true)
{
$this->_value = $this->getCheckedValue();
$this->_checked = true;
return;
}
if($value == $this->getCheckedValue())
{
$this->_value = $this->getCheckedValue();
$this->_checked = true;
return;
}
$this->_value = $value;
$this->_checked = false;
}
public function _onPreRender(View &$view)
{
parent::_onPreRender($view);
$view->checkedValue = $this->getCheckedValue();
$view->checked = $this->_checked;
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace b8\Form\Element;
use b8\Form\FieldSet;
class CheckboxGroup extends FieldSet
{
}

View file

@ -0,0 +1,28 @@
<?php
namespace b8\Form\Element;
use b8\Form\Element\Hidden,
b8\View;
class Csrf extends Hidden
{
protected $_rows = 4;
public function validate()
{
if($this->_value != $_COOKIE[$this->getName()])
{
return false;
}
return true;
}
protected function _onPreRender(View &$view)
{
parent::_onPreRender($view);
$csrf = md5(microtime(true));
$view->csrf = $csrf;
setcookie($this->getName(), $csrf);
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace b8\Form\Element;
use b8\View;
class Email extends Text
{
public function render($viewFile = null)
{
return parent::render(($viewFile ? $viewFile : 'Text'));
}
protected function _onPreRender(View &$view)
{
parent::_onPreRender($view);
$view->type = 'email';
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace b8\Form\Element;
use b8\Form\Input,
b8\View;
class Hidden extends Input
{
}

View file

@ -0,0 +1,19 @@
<?php
namespace b8\Form\Element;
use b8\Form\Element\Text,
b8\View;
class Password extends Text
{
public function render($viewFile = null)
{
return parent::render(($viewFile ? $viewFile : 'Text'));
}
protected function _onPreRender(View &$view)
{
parent::_onPreRender($view);
$view->type = 'password';
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace b8\Form\Element;
use b8\Form\Element\Select;
class Radio extends Select
{
}

View file

@ -0,0 +1,21 @@
<?php
namespace b8\Form\Element;
use b8\View,
b8\Form\Input;
class Select extends Input
{
protected $_options = array();
public function setOptions(array $options)
{
$this->_options = $options;
}
protected function _onPreRender(View &$view)
{
parent::_onPreRender($view);
$view->options = $this->_options;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace b8\Form\Element;
use b8\Form\Element\Button,
b8\View;
class Submit extends Button
{
protected $_value = 'Submit';
public function render($viewFile = null)
{
return parent::render(($viewFile ? $viewFile : 'Button'));
}
protected function _onPreRender(View &$view)
{
parent::_onPreRender($view);
$view->type = 'submit';
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace b8\Form\Element;
use b8\Form\Input,
b8\View;
class Text extends Input
{
protected function _onPreRender(View &$view)
{
parent::_onPreRender($view);
$view->type = 'text';
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace b8\Form\Element;
use b8\Form\Element\Text,
b8\View;
class TextArea extends Text
{
protected $_rows = 4;
public function getRows()
{
return $this->_rows;
}
public function setRows($rows)
{
$this->_rows = $rows;
}
protected function _onPreRender(View &$view)
{
parent::_onPreRender($view);
$view->rows = $this->getRows();
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace b8\Form\Element;
use b8\View;
class Url extends Text
{
public function render($viewFile = null)
{
return parent::render(($viewFile ? $viewFile : 'Text'));
}
protected function _onPreRender(View &$view)
{
parent::_onPreRender($view);
$view->type = 'url';
}
}

114
src/B8Framework/Form/FieldSet.php Executable file
View file

@ -0,0 +1,114 @@
<?php
namespace b8\Form;
use b8\Form\Element,
b8\Form\Input,
b8\View;
class FieldSet extends Element
{
protected $_children = array();
public function getValues()
{
$rtn = array();
foreach($this->_children as $field)
{
if($field instanceof FieldSet)
{
$fieldName = $field->getName();
if(empty($fieldName))
{
$rtn = array_merge($rtn, $field->getValues());
}
else
{
$rtn[$fieldName] = $field->getValues();
}
}
elseif($field instanceof Input)
{
if($field->getName())
{
$rtn[$field->getName()] = $field->getValue();
}
}
}
return $rtn;
}
public function setValues(array $values)
{
foreach($this->_children as $field)
{
if($field instanceof FieldSet)
{
$fieldName = $field->getName();
if(empty($fieldName) || !isset($values[$fieldName]))
{
$field->setValues($values);
}
else
{
$field->setValues($values[$fieldName]);
}
}
elseif($field instanceof Input)
{
$fieldName = $field->getName();
if(isset($values[$fieldName]))
{
$field->setValue($values[$fieldName]);
}
}
}
}
public function addField(Element $field)
{
$this->_children[$field->getName()] = $field;
$field->setParent($this);
}
public function validate()
{
$rtn = true;
foreach($this->_children as $child)
{
if(!$child->validate())
{
$rtn = false;
}
}
return $rtn;
}
protected function _onPreRender(View &$view)
{
$rendered = array();
foreach($this->_children as $child)
{
$rendered[] = $child->render();
}
$view->children = $rendered;
}
public function getChildren()
{
return $this->_children;
}
public function getChild($fieldName)
{
return $this->_children[$fieldName];
}
}

124
src/B8Framework/Form/Input.php Executable file
View file

@ -0,0 +1,124 @@
<?php
namespace b8\Form;
use b8\Form\Element,
b8\View;
class Input extends Element
{
protected $_required = false;
protected $_pattern;
protected $_validator;
protected $_value;
protected $_error;
protected $_customError = false;
public static function create($name, $label, $required = false)
{
$el = new static();
$el->setName($name);
$el->setLabel($label);
$el->setRequired($required);
return $el;
}
public function getValue()
{
return $this->_value;
}
public function setValue($value)
{
$this->_value = $value;
return $this;
}
public function getRequired()
{
return $this->_required;
}
public function setRequired($required)
{
$this->_required = (bool)$required;
return $this;
}
public function getValidator()
{
return $this->_validator;
}
public function setValidator($validator)
{
if(is_callable($validator) || $validator instanceof \Closure)
{
$this->_validator = $validator;
}
return $this;
}
public function getPattern()
{
return $this->_pattern;
}
public function setPattern($pattern)
{
$this->_pattern = $pattern;
return $this;
}
public function validate()
{
if($this->getRequired() && empty($this->_value))
{
$this->_error = $this->getLabel() . ' is required.';
return false;
}
if($this->getPattern() && !preg_match('/'.$this->getPattern().'/', $this->_value))
{
$this->_error = 'Invalid value entered.';
return false;
}
$validator = $this->getValidator();
if(is_callable($validator))
{
try
{
call_user_func_array($validator, array($this->_value));
}
catch(\Exception $ex)
{
$this->_error = $ex->getMessage();
return false;
}
}
if ($this->_customError) {
return false;
}
return true;
}
public function setError($message)
{
$this->_customError = true;
$this->_error = $message;
return $this;
}
protected function _onPreRender(View &$view)
{
$view->value = $this->getValue();
$view->error = $this->_error;
$view->pattern = $this->_pattern;
$view->required = $this->_required;
}
}

View file

@ -0,0 +1 @@
<input class="btn <?php print $css; ?>" type="<?php print $type; ?>" value="<?php print $value; ?>">

View file

@ -0,0 +1,17 @@
<?php if(!($parent instanceof \b8\Form\Element\CheckboxGroup)): ?>
<div class="control-group <?php print $ccss ?> <?php print (isset($error) ? 'error' : ''); ?>">
<div class="controls">
<div class="checkbox">
<?php endif; ?>
<label class="checkbox <?php print $css; ?>" for="<?php print $id ?>">
<input type="checkbox" id="<?php print $id; ?>" name="<?php print $name; ?>" value="<?php print $checkedValue; ?>" <?php print ($checked ? 'checked' : ''); ?> <?php print $required ? 'required' : '' ?>>
<?php print $label; ?>
</label>
<?php if(isset($error)): ?>
<span class="help-block"><?php print $error; ?></span>
<?php endif; ?>
<?php if(!($parent instanceof \b8\Form\Element\CheckboxGroup)): ?>
</div>
</div>
</div>
<?php endif; ?>

View file

@ -0,0 +1,11 @@
<div class="control-group <?php print $css; ?>">
<?php if($label): ?>
<label class="control-label"><?php print $label; ?></label>
<?php endif; ?>
<div class="controls">
<?php foreach($children as $field): ?>
<?php print $field; ?>
<?php endforeach; ?>
</div>
</div>

View file

@ -0,0 +1,6 @@
<div class="control-group <?php print $css; ?>">
<?php foreach($children as $field): ?>
<?php print $field; ?>
<?php endforeach; ?>
</div>

View file

@ -0,0 +1 @@
<input type="hidden" id="<?php print $id; ?>" name="<?php print $name; ?>" value="<?php print $csrf; ?>">

View file

@ -0,0 +1,9 @@
<fieldset class="row <?php print $css; ?>">
<?php if($label): ?>
<legend><?php print $label; ?></legend>
<?php endif; ?>
<?php foreach($children as $field): ?>
<?php print $field; ?>
<?php endforeach; ?>
</fieldset>

View file

@ -0,0 +1,6 @@
<form id="<?php print $id; ?>" class="<?php print $css; ?>" action="<?php print $action; ?>" method="<?php print $method; ?>">
<?php foreach($children as $field): ?>
<?php print $field; ?>
<?php endforeach; ?>
</form>

View file

@ -0,0 +1 @@
<input type="hidden" id="<?php print $id; ?>" name="<?php print $name; ?>" value="<?php print $value; ?>">

View file

@ -0,0 +1,18 @@
<div id="<?php print $id; ?>" class="control-group <?php print $ccss; ?>">
<?php if($label): ?>
<label class="control-label"><?php print $label; ?></label>
<?php endif; ?>
<div class="controls">
<?php foreach($options as $val => $lbl): ?>
<label class="radio" for="radio-<?php print $id; ?>-<?php print $val; ?>">
<input type="radio" id="radio-<?php print $id; ?>-<?php print $val; ?>" class="<?php print $css; ?>" name="<?php print $name; ?>" value="<?php print $val; ?>" <?php print ($value == $val) ? ' checked="checked"' : ''; ?> <?php print $required ? 'required' : '' ?>>
<?php print $lbl; ?>
</label>
<?php endforeach; ?>
<?php if(isset($error)): ?>
<span class="help-block"><?php print $error; ?></span>
<?php endif; ?>
</div>
</div>

View file

@ -0,0 +1,17 @@
<div class="control-group <?php print $ccss; ?>">
<?php if($label): ?>
<label class="control-label" for="<?php print $id ?>"><?php print $label; ?></label>
<?php endif; ?>
<div class="controls">
<select id="<?php print $id; ?>" class="<?php print $css; ?>" name="<?php print $name; ?>">
<?php foreach($options as $val => $lbl): ?>
<option value="<?php print $val; ?>" <?php print ($value == $val) ? ' selected="selected"' : ''; ?>><?php print $lbl; ?></option>
<?php endforeach; ?>
</select>
<?php if(isset($error)): ?>
<span class="help-block"><?php print $error; ?></span>
<?php endif; ?>
</div>
</div>

View file

@ -0,0 +1,13 @@
<div class="control-group <?php print $ccss; ?> <?php print (isset($error) ? 'error' : ''); ?>">
<?php if($label): ?>
<label class="control-label" for="<?php print $id ?>"><?php print $label; ?></label>
<?php endif; ?>
<div class="controls">
<input id="<?php print $id; ?>" type="<?php print $type; ?>" class="<?php print $css; ?>" name="<?php print $name; ?>" <?php print isset($value) ? ' value="' . $value . '"' : '' ?> <?php print isset($pattern) ? ' pattern="' . $pattern . '"' : '' ?> <?php print $required ? ' required' : '' ?>>
<?php if(isset($error)): ?>
<span class="help-block"><?php print $error; ?></span>
<?php endif; ?>
</div>
</div>

View file

@ -0,0 +1,13 @@
<div class="control-group <?php print $ccss; ?> <?php print (isset($error) ? 'error' : ''); ?>">
<?php if($label): ?>
<label class="control-label" for="<?php print $id ?>"><?php print $label; ?></label>
<?php endif; ?>
<div class="controls">
<textarea rows="<?php print $rows; ?>" id="<?php print $id; ?>" class="<?php print $css; ?>" name="<?php print $name; ?>" <?php print $required ? ' required' : '' ?>><?php print isset($value) ? $value : '' ?></textarea>
<?php if(isset($error)): ?>
<span class="help-block"><?php print $error; ?></span>
<?php endif; ?>
</div>
</div>

149
src/B8Framework/Http/Request.php Executable file
View file

@ -0,0 +1,149 @@
<?php
namespace b8\Http;
class Request
{
/**
* @var array
*/
protected $params = array();
/**
* Request data.
*/
protected $data = array();
/**
* Set up the request.
*/
public function __construct()
{
$this->parseInput();
$this->data['path'] = $this->getRequestPath();
$this->data['parts'] = array_values(array_filter(explode('/', $this->data['path'])));
}
protected function getRequestPath()
{
$path = '';
// Start out with the REQUEST_URI:
if (!empty($_SERVER['REQUEST_URI'])) {
$path = $_SERVER['REQUEST_URI'];
}
if ($_SERVER['SCRIPT_NAME'] != $_SERVER['REQUEST_URI']) {
$scriptPath = str_replace('/index.php', '', $_SERVER['SCRIPT_NAME']);
$path = str_replace($scriptPath, '', $path);
}
// Remove index.php from the URL if it is present:
$path = str_replace(array('/index.php', 'index.php'), '', $path);
// Also cut out the query string:
$path = explode('?', $path);
$path = array_shift($path);
return $path;
}
/**
* Parse incoming variables, incl. $_GET, $_POST and also reads php://input for PUT/DELETE.
*/
protected function parseInput()
{
$params = $_REQUEST;
if(!isset($_SERVER['REQUEST_METHOD']) || in_array($_SERVER['REQUEST_METHOD'], array('PUT', 'DELETE')))
{
$vars = file_get_contents('php://input');
if(!is_string($vars) || strlen(trim($vars)) === 0)
{
$vars = '';
}
$inputData = array();
parse_str($vars, $inputData);
$params = array_merge($params, $inputData);
}
$this->setParams($params);
}
/**
* Returns all request parameters.
* @return array
*/
public function getParams()
{
return $this->params;
}
/**
* Return a specific request parameter, or a default value if not set.
*/
public function getParam($key, $default = null)
{
if (isset($this->params[$key])) {
return $this->params[$key];
} else {
return $default;
}
}
/**
* Set or override a request parameter.
*/
public function setParam($key, $value = null)
{
$this->params[$key] = $value;
}
/**
* Set an array of request parameters.
*/
public function setParams(array $params)
{
$this->params = array_merge($this->params, $params);
}
/**
* Un-set a specific parameter.
*/
public function unsetParam($key)
{
unset($this->params[$key]);
}
public function getMethod()
{
return strtoupper($_SERVER['REQUEST_METHOD']);
}
public function getPath()
{
return $this->data['path'];
}
public function getPathParts()
{
return $this->data['parts'];
}
public function isAjax()
{
if (!isset($_SERVER['HTTP_X_REQUESTED_WITH'])) {
return false;
}
if (strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
return true;
}
return false;
}
}

132
src/B8Framework/Http/Response.php Executable file
View file

@ -0,0 +1,132 @@
<?php
namespace b8\Http;
class Response
{
protected $data = array();
public function __construct(Response $createFrom = null)
{
if (!is_null($createFrom)) {
$this->data = $createFrom->getData();
}
}
public function hasLayout()
{
return !isset($this->data['layout']) ? true : $this->data['layout'];
}
public function disableLayout()
{
$this->data['layout'] = false;
}
public function enableLayout()
{
$this->data['layout'] = true;
}
public function getData()
{
return $this->data;
}
public function setResponseCode($code)
{
$this->data['code'] = (int)$code;
}
public function setHeader($key, $val)
{
$this->data['headers'][$key] = $val;
}
public function clearHeaders()
{
$this->data['headers'] = array();
}
public function setContent($content)
{
$this->data['body'] = $content;
}
public function getContent()
{
return $this->data['body'];
}
public function flush()
{
$this->sendResponseCode();
if (isset($this->data['headers'])) {
foreach ($this->data['headers'] as $header => $val) {
header($header . ': ' . $val, true);
}
}
return $this->flushBody();
}
protected function sendResponseCode()
{
if (!isset($this->data['code'])) {
$this->data['code'] = 200;
}
switch ($this->data['code'])
{
// 300 class
case 301:
$text = 'Moved Permanently';
break;
case 302:
$text = 'Moved Temporarily';
break;
// 400 class errors
case 400:
$text = 'Bad Request';
break;
case 401:
$text = 'Not Authorized';
break;
case 403:
$text = 'Forbidden';
break;
case 404:
$text = 'Not Found';
break;
// 500 class errors
case 500:
$text = 'Internal Server Error';
break;
// OK
case 200:
default:
$text = 'OK';
break;
}
header('HTTP/1.1 ' . $this->data['code'] . ' ' . $text, true, $this->data['code']);
}
protected function flushBody()
{
if (isset($this->data['body'])) {
return $this->data['body'];
}
return '';
}
public function __toString()
{
return $this->flush();
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace b8\Http\Response;
use b8\Http\Response;
class JsonResponse extends Response
{
public function __construct(Response $createFrom = null)
{
parent::__construct($createFrom);
$this->setContent(array());
$this->setHeader('Content-Type', 'application/json');
}
public function hasLayout()
{
return false;
}
protected function flushBody()
{
if (isset($this->data['body'])) {
return json_encode($this->data['body']);
}
return json_encode(null);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace b8\Http\Response;
use b8\Http\Response;
class RedirectResponse extends Response
{
public function __construct(Response $createFrom = null)
{
parent::__construct($createFrom);
$this->setContent(null);
$this->setResponseCode(302);
}
public function hasLayout()
{
return false;
}
public function flush()
{
parent::flush();
die;
}
}

123
src/B8Framework/Http/Router.php Executable file
View file

@ -0,0 +1,123 @@
<?php
namespace b8\Http;
use b8\Application;
use b8\Config;
use b8\Http\Request;
class Router
{
/**
* @var \b8\Http\Request;
*/
protected $request;
/**
* @var \b8\Http\Config;
*/
protected $config;
/**
* @var \b8\Application
*/
protected $application;
/**
* @var array
*/
protected $routes = array(array('route' => '/:controller/:action', 'callback' => null, 'defaults' => array()));
public function __construct(Application $application, Request $request, Config $config)
{
$this->application = $application;
$this->request = $request;
$this->config = $config;
}
public function clearRoutes()
{
$this->routes = array();
}
/**
* @param string $route Route definition
* @param array $options
* @param callable $callback
* @throws \InvalidArgumentException
*/
public function register($route, $options = array(), $callback = null)
{
if (!is_callable($callback)) {
throw new \InvalidArgumentException('$callback must be callable.');
}
array_unshift($this->routes, array('route' => $route, 'callback' => $callback, 'defaults' => $options));
}
public function dispatch()
{
foreach ($this->routes as $route) {
$pathParts = $this->request->getPathParts();
//-------
// Set up default values for everything:
//-------
$thisNamespace = 'Controller';
$thisController = null;
$thisAction = null;
if (array_key_exists('namespace', $route['defaults'])) {
$thisNamespace = $route['defaults']['namespace'];
}
if (array_key_exists('controller', $route['defaults'])) {
$thisController = $route['defaults']['controller'];
}
if (array_key_exists('action', $route['defaults'])) {
$thisAction = $route['defaults']['action'];
}
$routeParts = array_filter(explode('/', $route['route']));
$routeMatches = true;
while (count($routeParts)) {
$routePart = array_shift($routeParts);
$pathPart = array_shift($pathParts);
switch ($routePart) {
case ':namespace':
$thisNamespace = !is_null($pathPart) ? $pathPart : $thisNamespace;
break;
case ':controller':
$thisController = !is_null($pathPart) ? $pathPart : $thisController;
break;
case ':action':
$thisAction = !is_null($pathPart) ? $pathPart : $thisAction;
break;
default:
if ($routePart != $pathPart) {
$routeMatches = false;
}
}
if (!$routeMatches || !count($pathParts)) {
break;
}
}
$thisArgs = $pathParts;
if ($routeMatches) {
$route = array('namespace' => $thisNamespace, 'controller' => $thisController, 'action' => $thisAction, 'args' => $thisArgs, 'callback' => $route['callback']);
if ($this->application->isValidRoute($route)) {
return $route;
}
}
}
return null;
}
}

188
src/B8Framework/HttpClient.php Executable file
View file

@ -0,0 +1,188 @@
<?php
namespace b8;
class HttpClient
{
protected $_base = '';
protected $_params = array();
protected $_headers = array();
public function __construct($base = null)
{
$settings = Config::getInstance()->get('b8.http.client', array('base_url' => '', 'params' => array()));
$this->_base = $settings['base_url'];
$this->_params = isset($settings['params']) && is_array($settings['params']) ? $settings['params'] : array();
$this->_headers = array('Content-Type: application/x-www-form-urlencoded');
if(!is_null($base))
{
$this->_base = $base;
}
}
public function setHeaders(array $headers)
{
$this->_headers = $headers;
}
public function request($method, $uri, $params = array())
{
// Clean incoming:
$method = strtoupper($method);
$getParams = $this->_params;
if($method == 'GET' || $method == 'DELETE')
{
$getParams = array_merge($getParams, $params);
}
else
{
$bodyParams = is_array($params) ? http_build_query($params) : $params;
}
$getParams = http_build_query($getParams);
if(substr($uri, 0, 1) != '/' && !empty($this->_base))
{
$uri = '/' . $uri;
}
// Build HTTP context array:
$context = array();
$context['http']['user_agent'] = 'b8/1.0';
$context['http']['timeout'] = 30;
$context['http']['method'] = $method;
$context['http']['ignore_errors'] = true;
$context['http']['header'] = implode(PHP_EOL, $this->_headers);
if(in_array($method, array('PUT', 'POST')))
{
$context['http']['content'] = $bodyParams;
}
$uri .= '?' . $getParams;
$context = stream_context_create($context);
$result = file_get_contents($this->_base . $uri, false, $context);
$res = array();
$res['headers'] = $http_response_header;
$res['code'] = (int)preg_replace('/HTTP\/1\.[0-1] ([0-9]+)/', '$1', $res['headers'][0]);
$res['success'] = false;
$res['body'] = $this->_decodeResponse($result);
if($res['code'] >= 200 && $res['code'] < 300)
{
$res['success'] = true;
}
// Handle JSON responses:
foreach($res['headers'] as $header)
{
if(stripos($header, 'Content-Type') !== false || stripos($header, 'b8-Type') !== false)
{
if(stripos($header, 'application/json') !== false)
{
$res['text_body'] = $res['body'];
$res['body'] = json_decode($res['body'], true);
}
}
}
return $res;
}
public function get($uri, $params = array())
{
return $this->request('GET', $uri, $params);
}
public function put($uri, $params = array())
{
return $this->request('PUT', $uri, $params);
}
public function post($uri, $params = array())
{
return $this->request('POST', $uri, $params);
}
public function delete($uri, $params = array())
{
return $this->request('DELETE', $uri, $params);
}
protected function _decodeResponse($originalResponse)
{
$response = $originalResponse;
$body = '';
do
{
$line = $this->_readChunk($response);
if($line == PHP_EOL)
{
continue;
}
$length = hexdec(trim($line));
if(!is_int($length) || empty($response) || $line === false || $length < 1)
{
break;
}
do
{
$data = $this->_readChunk($response, $length);
// remove the amount received from the total length on the next loop
// it'll attempt to read that much less data
$length -= strlen($data);
// store in string for later use
$body .= $data;
// zero or less or end of connection break
if($length <= 0 || empty($response))
{
break;
}
}
while(true);
}
while(true);
if(empty($body))
{
$body = $originalResponse;
}
return $body;
}
function _readChunk(&$string, $len = 4096)
{
$rtn = '';
for($i = 0; $i <= $len; $i++)
{
if(empty($string))
{
break;
}
$char = $string[0];
$string = substr($string, 1);
$rtn .= $char;
if($char == PHP_EOL)
{
break;
}
}
return $rtn;
}
}

149
src/B8Framework/Image.php Executable file
View file

@ -0,0 +1,149 @@
<?php
namespace b8;
class Image
{
public static $cachePath = '/tmp/';
public static $sourcePath = './';
/**
* @var \Imagick
*/
protected $source;
public function __construct($imagePath)
{
$this->setSource(new \Imagick(self::$sourcePath . $imagePath));
}
/**
* @return \Imagick
*/
public function getSource()
{
return $this->source;
}
/**
* @param \Imagick $image
*/
public function setSource(\Imagick $image)
{
$this->source = $image;
}
public function render($media, $width, $height, $format = 'jpeg')
{
$cachePath = self::$cachePath . $media['fileId'] . '.' . $width . 'x' . $height . '.' . $format;
if(file_exists($cachePath) && 0)
{
$output = file_get_contents($cachePath);
}
else
{
$output = $this->doRender($media, $width, $height, $format);
file_put_contents($cachePath, $output);
}
return $output;
}
public function doRender($media, $width, $height, $format = 'jpeg')
{
$focal = !empty($media['focal_point']) ? $media['focal_point'] : array(0, 0);
$focalX = (int)$focal[0];
$focalY = (int)$focal[1];
$width = (int)$width;
$height = (int)$height;
$source = $this->getSource();
$sourceWidth = $source->getImageWidth();
$sourceHeight = $source->getImageHeight();
$sourceRatio = $sourceWidth / $sourceHeight;
$targetRatio = $height != 'auto' ? $width / $height : $sourceRatio;
$quads = $this->_getQuadrants($sourceWidth, $sourceHeight);
foreach($quads as $name => $l)
{
if($focalX >= $l[0] && $focalX <= $l[1] && $focalY >= $l[2] && $focalY <= $l[3])
{
$useQuad = $name;
}
}
if($sourceRatio <= $targetRatio)
{
$scale = $sourceWidth / $width;
}
else
{
$scale = $sourceHeight / $height;
}
$resizeWidth = (int)($sourceWidth / $scale);
$resizeHeight = (int)($sourceHeight / $scale);
if($height == 'auto')
{
$height = $resizeHeight;
}
$source->scaleImage($resizeWidth, $resizeHeight);
switch($useQuad)
{
case 'top_left':
$cropX = 0;
$cropY = 0;
break;
case 'top_right':
$cropX = ($resizeWidth - $width);
$cropY = 0;
break;
case 'middle_left':
$cropX = 0;
$cropY = ($resizeHeight - $height) / 2;
break;
case 'middle-right':
$cropX = ($resizeWidth - $width);
$cropY = ($resizeHeight - $height) / 2;
break;
case 'bottom_left':
$cropX = 0;
$cropY = ($resizeHeight - $height);
break;
case 'bottom_right':
$cropX = ($resizeWidth - $width);
$cropY = ($resizeHeight - $height);
break;
}
$source->cropImage($width, $height, $cropX, $cropY);
$source->setImageFormat($format);
return $source;
}
protected function _getQuadrants($x, $y)
{
$rtn = array();
$rtn['top_left'] = array(0, $x / 2, 0, $y / 3);
$rtn['top_right'] = array(($x / 2) + 1, $x, 0, $y / 3);
$rtn['middle_left'] = array(0, $y / 2, ($y / 3)+1, (($y / 3) * 2));
$rtn['middle_right'] = array(($x / 2) + 1, $x, ($y / 3)+1, (($y / 3) * 2));
$rtn['bottom_left'] = array(0, $y / 2, (($y / 3) * 2)+1, $y);
$rtn['bottom_right'] = array(($x / 2) + 1, $x, (($y / 3) * 2)+1, $y);
return $rtn;
}
}

224
src/B8Framework/Model.php Executable file
View file

@ -0,0 +1,224 @@
<?php
namespace b8;
use b8\Exception\HttpException;
use b8\Cache;
class Model
{
public static $sleepable = array();
protected $getters = array();
protected $setters = array();
protected $data = array();
protected $modified = array();
protected $tableName;
protected $cache;
public function __construct($initialData = array())
{
if(is_array($initialData))
{
$this->data = array_merge($this->data, $initialData);
}
$this->cache = Cache::getCache(Cache::TYPE_REQUEST);
}
public function getTableName()
{
return $this->tableName;
}
public function toArray($depth = 2, $currentDepth = 0)
{
if(isset(static::$sleepable) && is_array(static::$sleepable) && count(static::$sleepable))
{
$sleepable = static::$sleepable;
}
else
{
$sleepable = array_keys($this->getters);
}
$rtn = array();
foreach($sleepable as $property)
{
$rtn[$property] = $this->_propertyToArray($property, $currentDepth, $depth);
}
return $rtn;
}
protected function _propertyToArray($property, $currentDepth, $depth)
{
$rtn = null;
if(array_key_exists($property, $this->getters))
{
$method = $this->getters[$property];
$rtn = $this->{$method}();
if(is_object($rtn) || is_array($rtn))
{
$rtn = ($depth > $currentDepth) ? $this->_valueToArray($rtn, $currentDepth, $depth) : null;
}
}
return $rtn;
}
protected function _valueToArray($value, $currentDepth, $depth)
{
$rtn = null;
if(!is_null($value))
{
if(is_object($value) && method_exists($value, 'toArray'))
{
$rtn = $value->toArray($depth, $currentDepth + 1);
}
elseif(is_array($value))
{
$childArray = array();
foreach($value as $k => $v)
{
$childArray[$k] = $this->_valueToArray($v, $currentDepth + 1, $depth);
}
$rtn = $childArray;
}
else
{
$rtn = (is_string($value) && !mb_check_encoding($value, 'UTF-8')) ? mb_convert_encoding($value, 'UTF-8') : $value;
}
}
return $rtn;
}
public function getDataArray()
{
return $this->data;
}
public function getModified()
{
return $this->modified;
}
public function setValues(array $values)
{
foreach($values as $key => $value)
{
if(isset($this->setters[$key]))
{
$func = $this->setters[$key];
if($value === 'null')
{
$value = null;
}
elseif($value === 'true')
{
$value = true;
}
elseif($value === 'false')
{
$value = false;
}
$this->{$func}($value);
}
}
}
protected function _setModified($column)
{
$this->modified[$column] = $column;
}
//----------------
// Validation
//----------------
protected function _validateString($name, $value)
{
if(!is_string($value) && !is_null($value))
{
throw new HttpException\ValidationException($name . ' must be a string.');
}
}
protected function _validateInt($name, &$value)
{
if(is_bool($value))
{
$value = $value ? 1 : 0;
}
if(!is_numeric($value) && !is_null($value))
{
throw new HttpException\ValidationException($name . ' must be an integer.');
}
if(!is_int($value) && !is_null($value))
{
$value = (int)$value;
}
}
protected function _validateFloat($name, &$value)
{
if(!is_numeric($value) && !is_null($value))
{
throw new HttpException\ValidationException($name . ' must be a float.');
}
if(!is_float($value) && !is_null($value))
{
$value = (float)$value;
}
}
protected function _validateDate($name, &$value)
{
if(is_string($value))
{
$value = empty($value) ? null : new \DateTime($value);
}
if((!is_object($value) || !($value instanceof \DateTime)) && !is_null($value))
{
throw new HttpException\ValidationException($name . ' must be a date object.');
}
$value = empty($value) ? null : $value->format('Y-m-d H:i:s');
}
protected function _validateNotNull($name, &$value)
{
if(is_null($value))
{
throw new HttpException\ValidationException($name . ' must not be null.');
}
}
public function __get($key)
{
if (array_key_exists($key, $this->getters)) {
$getter = $this->getters[$key];
return $this->{$getter}();
}
return null;
}
public function __set($key, $value)
{
if (array_key_exists($key, $this->setters)) {
$setter = $this->setters[$key];
return $this->{$setter}($value);
}
}
}

379
src/B8Framework/Store.php Executable file
View file

@ -0,0 +1,379 @@
<?php
namespace b8;
use b8\Exception\HttpException;
use b8\Database,
b8\Model;
abstract class Store
{
protected $modelName = null;
protected $tableName = null;
protected $primaryKey = null;
/**
* @return \b8\Model
*/
abstract public function getByPrimaryKey($key, $useConnection = 'read');
public function getWhere($where = array(), $limit = 25, $offset = 0, $joins = array(), $order = array(), $manualJoins = array(), $group = null, $manualWheres = array(), $whereType = 'AND')
{
$query = 'SELECT ' . $this->tableName . '.* FROM ' . $this->tableName;
$countQuery = 'SELECT COUNT(*) AS cnt FROM ' . $this->tableName;
$wheres = array();
$params = array();
foreach($where as $key => $value)
{
$key = $this->fieldCheck($key);
if(!is_array($value))
{
$params[] = $value;
$wheres[] = $key . ' = ?';
}
else
{
if(isset($value['operator']))
{
if(is_array($value['value']))
{
if($value['operator'] == 'between')
{
$params[] = $value['value'][0];
$params[] = $value['value'][1];
$wheres[] = $key . ' BETWEEN ? AND ?';
}
elseif($value['operator'] == 'IN')
{
$in = array();
foreach($value['value'] as $item)
{
$params[] = $item;
$in[] = '?';
}
$wheres[] = $key . ' IN (' . implode(', ', $in) . ') ';
}
else
{
$ors = array();
foreach($value['value'] as $item)
{
if($item == 'null')
{
switch($value['operator'])
{
case '!=':
$ors[] = $key . ' IS NOT NULL';
break;
case '==':
default:
$ors[] = $key . ' IS NULL';
break;
}
}
else
{
$params[] = $item;
$ors[] = $this->fieldCheck($key) . ' ' . $value['operator'] . ' ?';
}
}
$wheres[] = '(' . implode(' OR ', $ors) . ')';
}
}
else
{
if($value['operator'] == 'like')
{
$params[] = '%' . $value['value'] . '%';
$wheres[] = $key . ' ' . $value['operator'] . ' ?';
}
else
{
if($value['value'] === 'null')
{
switch($value['operator'])
{
case '!=':
$wheres[] = $key . ' IS NOT NULL';
break;
case '==':
default:
$wheres[] = $key . ' IS NULL';
break;
}
}
else
{
$params[] = $value['value'];
$wheres[] = $key . ' ' . $value['operator'] . ' ?';
}
}
}
}
else
{
$wheres[] = $key . ' IN (' . implode(', ', array_map(array(Database::getConnection('read'), 'quote'), $value)) . ')';
}
}
}
if(count($joins))
{
foreach($joins as $table => $join)
{
$query .= ' LEFT JOIN ' . $table . ' ' . $join['alias'] . ' ON ' . $join['on'] . ' ';
$countQuery .= ' LEFT JOIN ' . $table . ' ' . $join['alias'] . ' ON ' . $join['on'] . ' ';
}
}
if(count($manualJoins))
{
foreach($manualJoins as $join)
{
$query .= ' ' . $join . ' ';
$countQuery .= ' ' . $join . ' ';
}
}
$hasWhere = false;
if(count($wheres))
{
$hasWhere = true;
$query .= ' WHERE (' . implode(' ' . $whereType . ' ', $wheres) . ')';
$countQuery .= ' WHERE (' . implode(' ' . $whereType . ' ', $wheres) . ')';
}
if(count($manualWheres))
{
foreach($manualWheres as $where)
{
if(!$hasWhere)
{
$hasWhere = true;
$query .= ' WHERE ';
$countQuery .= ' WHERE ';
}
else
{
$query .= ' ' . $where['type'] . ' ';
$countQuery .= ' ' . $where['type'] . ' ';
}
$query .= ' ' . $where['query'];
$countQuery .= ' ' . $where['query'];
if(isset($where['params']))
{
foreach($where['params'] as $param)
{
$params[] = $param;
}
}
}
}
if(!is_null($group))
{
$query .= ' GROUP BY ' . $group . ' ';
}
if(count($order))
{
$orders = array();
if(is_string($order) && $order == 'rand')
{
$query .= ' ORDER BY RAND() ';
}
else
{
foreach($order as $key => $value)
{
$orders[] = $this->fieldCheck($key) . ' ' . $value;
}
$query .= ' ORDER BY ' . implode(', ', $orders);
}
}
if($limit)
{
$query .= ' LIMIT ' . $limit;
}
if($offset)
{
$query .= ' OFFSET ' . $offset;
}
try
{
$stmt = Database::getConnection('read')->prepare($countQuery);
$stmt->execute($params);
$res = $stmt->fetch(\PDO::FETCH_ASSOC);
$count = (int)$res['cnt'];
}
catch(\PDOException $ex)
{
$count = 0;
}
try
{
$stmt = Database::getConnection('read')->prepare($query);
$stmt->execute($params);
$res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$rtn = array();
foreach($res as $data)
{
$rtn[] = new $this->modelName($data);
}
return array('items' => $rtn, 'count' => $count);
}
catch(\PDOException $ex)
{
throw $ex;
}
}
public function save(Model $obj, $saveAllColumns = false)
{
if(!isset($this->primaryKey))
{
throw new HttpException\BadRequestException('Save not implemented for this store.');
}
if(!($obj instanceof $this->modelName))
{
throw new HttpException\BadRequestException(get_class($obj) . ' is an invalid model type for this store.');
}
$data = $obj->getDataArray();
if(isset($data[$this->primaryKey]))
{
$rtn = $this->saveByUpdate($obj, $saveAllColumns);
}
else
{
$rtn = $this->saveByInsert($obj, $saveAllColumns);
}
return $rtn;
}
public function saveByUpdate(Model $obj, $saveAllColumns = false)
{
$rtn = null;
$data = $obj->getDataArray();
$modified = ($saveAllColumns) ? array_keys($data) : $obj->getModified();
$updates = array();
$update_params = array();
foreach($modified as $key)
{
$updates[] = $key . ' = :' . $key;
$update_params[] = array($key, $data[$key]);
}
if(count($updates))
{
$qs = 'UPDATE ' . $this->tableName . '
SET ' . implode(', ', $updates) . '
WHERE ' . $this->primaryKey . ' = :primaryKey';
$q = Database::getConnection('write')->prepare($qs);
foreach($update_params as $update_param)
{
$q->bindValue(':' . $update_param[0], $update_param[1]);
}
$q->bindValue(':primaryKey', $data[$this->primaryKey]);
$q->execute();
$rtn = $this->getByPrimaryKey($data[$this->primaryKey], 'write');
}
else
{
$rtn = $obj;
}
return $rtn;
}
public function saveByInsert(Model $obj, $saveAllColumns = false)
{
$rtn = null;
$data = $obj->getDataArray();
$modified = ($saveAllColumns) ? array_keys($data) : $obj->getModified();
$cols = array();
$values = array();
$qParams = array();
foreach($modified as $key)
{
$cols[] = $key;
$values[] = ':' . $key;
$qParams[':' . $key] = $data[$key];
}
if(count($cols))
{
$qs = 'INSERT INTO ' . $this->tableName . ' (' . implode(', ', $cols) . ') VALUES (' . implode(', ', $values) . ')';
$q = Database::getConnection('write')->prepare($qs);
if($q->execute($qParams))
{
$id = !empty($data[$this->primaryKey]) ? $data[$this->primaryKey] : Database::getConnection('write')->lastInsertId();
$rtn = $this->getByPrimaryKey($id, 'write');
}
}
return $rtn;
}
public function delete(Model $obj)
{
if(!isset($this->primaryKey))
{
throw new HttpException\BadRequestException('Delete not implemented for this store.');
}
if(!($obj instanceof $this->modelName))
{
throw new HttpException\BadRequestException(get_class($obj) . ' is an invalid model type for this store.');
}
$data = $obj->getDataArray();
$q = Database::getConnection('write')->prepare('DELETE FROM ' . $this->tableName . ' WHERE ' . $this->primaryKey . ' = :primaryKey');
$q->bindValue(':primaryKey', $data[$this->primaryKey]);
$q->execute();
return true;
}
/**
*
*/
protected function fieldCheck($field)
{
if(empty($field))
{
throw new HttpException('You cannot have an empty field name.');
}
if(strpos($field, '.') === false)
{
return $this->tableName . '.' . $field;
}
return $field;
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace b8\Store;
use b8\Config;
class Factory
{
/**
* @var \b8\Store\Factory
*/
protected static $instance;
/**
* A collection of the stores currently loaded by the factory.
* @var \b8\Store[]
*/
protected $loadedStores = array();
/**
* @return Factory
*/
public static function getInstance()
{
if(!isset(self::$instance))
{
self::$instance = new self();
}
return self::$instance;
}
/**
* @param $storeName string Store name (should match a model name).
*
* @return \b8\Store
*/
public static function getStore($storeName, $namespace = null)
{
$factory = self::getInstance();
return $factory->loadStore($storeName, $namespace);
}
protected function __construct()
{
}
/**
* @param $store
*
* @return \b8\Store;
*/
public function loadStore($store, $namespace = null)
{
if(!isset($this->loadedStores[$store]))
{
$namespace = is_null($namespace) ? Config::getInstance()->get('b8.app.namespace') : $namespace;
$class = $namespace . '\\Store\\' . $store . 'Store';
$obj = new $class();
$this->loadedStores[$store] = $obj;
}
return $this->loadedStores[$store];
}
}

17
src/B8Framework/Type/Cache.php Executable file
View file

@ -0,0 +1,17 @@
<?php
namespace b8\Type;
interface Cache
{
public function get($key, $default = null);
public function set($key, $value = null, $ttl = 0);
public function delete($key);
public function contains($key);
public function isEnabled();
public function __get($key);
public function __set($key, $value = null);
public function __unset($key);
public function __isset($key);
}

View file

@ -0,0 +1,8 @@
<?php
namespace b8\Type;
interface RestUser
{
public function checkPermission($permission, $resource);
}

86
src/B8Framework/View.php Executable file
View file

@ -0,0 +1,86 @@
<?php
namespace b8;
use b8\Exception\HttpException;
class View
{
protected $_vars = array();
protected static $_helpers = array();
protected static $extension = 'phtml';
public function __construct($file, $path = null)
{
if (!self::exists($file, $path)) {
throw new \Exception('View file does not exist: ' . $file);
}
$this->viewFile = self::getViewFile($file, $path);
}
protected static function getViewFile($file, $path = null)
{
$viewPath = is_null($path) ? Config::getInstance()->get('b8.view.path') : $path;
$fullPath = $viewPath . $file . '.' . static::$extension;
return $fullPath;
}
public static function exists($file, $path = null)
{
if (!file_exists(self::getViewFile($file, $path))) {
return false;
}
return true;
}
public function __isset($var)
{
return isset($this->_vars[$var]);
}
public function __get($var)
{
return $this->_vars[$var];
}
public function __set($var, $val)
{
$this->_vars[$var] = $val;
}
public function __call($method, $params = array())
{
if(!isset(self::$_helpers[$method]))
{
$class = '\\' . Config::getInstance()->get('b8.app.namespace') . '\\Helper\\' . $method;
if(!class_exists($class))
{
$class = '\\b8\\View\\Helper\\' . $method;
}
if(!class_exists($class))
{
throw new HttpException('Helper class does not exist: ' . $class);
}
self::$_helpers[$method] = new $class();
}
return self::$_helpers[$method];
}
public function render()
{
extract($this->_vars);
ob_start();
require($this->viewFile);
$html = ob_get_contents();
ob_end_clean();
return $html;
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace b8\View\Helper;
class Format
{
public function Currency($number, $symbol = true)
{
return ($symbol ? '£' : '') . number_format($number, 2, '.', ',');
}
}

544
src/B8Framework/View/Template.php Executable file
View file

@ -0,0 +1,544 @@
<?php
namespace b8\View;
use b8\View;
class Template extends View
{
public static $templateFunctions = array();
protected static $extension = 'html';
public function __construct($viewCode)
{
$this->viewCode = $viewCode;
if (!count(self::$templateFunctions)) {
self::$templateFunctions = array('include' => array($this, 'includeTemplate'), 'call' => array($this, 'callHelperFunction'));
}
}
public static function createFromFile($file, $path = null)
{
if (!static::exists($file, $path)) {
throw new \Exception('View file does not exist: ' . $file);
}
$viewFile = static::getViewFile($file, $path);
return new static(file_get_contents($viewFile));
}
public static function createFromString($string)
{
return new static($string);
}
public function addFunction($name, $handler)
{
self::$templateFunctions[$name] = $handler;
}
public function removeFunction($name)
{
unset(self::$templateFunctions[$name]);
}
public function render()
{
return $this->parse($this->viewCode);
}
protected function parse($string)
{
$lastCond = null;
$keywords = array('ifnot', 'if', 'else', 'for', 'loop', '@', '/ifnot', '/if', '/for', '/loop');
foreach (self::$templateFunctions as $function => $handler) {
$keywords[] = $function;
}
$stack = array('children' => array(array('type' => 'string', 'body' => '')));
$stack['children'][0]['parent'] =& $stack;
$current =& $stack['children'][0];
while (!empty($string)) {
$current['body'] .= $this->readUntil('{', $string);
if (!empty($string)) {
$gotKeyword = false;
foreach($keywords as $keyword) {
$kwLen = strlen($keyword) + 1;
if (substr($string, 0, $kwLen) == '{' . $keyword) {
$gotKeyword = true;
$item = array('type' => $keyword, 'cond' => '', 'children' => '');
$string = substr($string, $kwLen);
$cond = trim($this->readUntil('}', $string));
$item['cond'] = $cond;
$lastCond = $cond;
$string = substr($string, 1);
if (array_key_exists($keyword, self::$templateFunctions)) {
$item['function_name'] = $keyword;
$item['type'] = 'function';
}
$str = array('type' => 'string', 'body' => '');
$parent =& $current['parent'];
if (substr($current['body'], (0 - strlen(PHP_EOL))) === PHP_EOL) {
$current['body'] = substr($current['body'], 0, strlen($current['body']) - strlen(PHP_EOL));
}
$item['parent'] =& $parent;
$parent['children'][] = $item;
if ($keyword == '@' || $item['type'] == 'function') {
// If we're processing a variable, add a string to the parent and move up to that as current.
$parent['children'][] = $str;
$current =& $parent['children'][count($parent['children']) - 1];
$current['parent'] =& $parent;
} elseif (substr($keyword, 0, 1) == '/') {
// If we're processing the end of a block (if/loop), add a string to the parent's parent and move up to that.
$parent =& $parent['parent'];
$parent['children'][] = $str;
$current =& $parent['children'][count($parent['children']) - 1];
$current['parent'] =& $parent;
} else {
$parent['children'][count($parent['children']) - 1]['children'][] = $str;
$current =& $parent['children'][count($parent['children']) - 1]['children'][0];
$current['parent'] =& $parent['children'][count($parent['children']) - 1];
}
break;
}
}
if (!$gotKeyword) {
$current['body'] .= substr($string, 0, 1);
$string = substr($string, 1);
}
}
}
return $this->processStack($stack);
}
protected function processStack($stack)
{
$res = '';
while (count($stack['children'])) {
$current = array_shift($stack['children']);
switch ($current['type']) {
case 'string':
$res .= $current['body'];
break;
case '@':
$res .= $this->doParseVar($current['cond']);
break;
case 'if':
$res .= $this->doParseIf($current['cond'], $current);
break;
case 'ifnot':
$res .= $this->doParseIfNot($current['cond'], $current);
break;
case 'loop':
$res .= $this->doParseLoop($current['cond'], $current);
break;
case 'for':
$res .= $this->doParseFor($current['cond'], $current);
break;
case 'function':
$res .= $this->doParseFunction($current);
break;
}
}
return $res;
}
protected function readUntil($until, &$string)
{
$read = '';
while (!empty($string)) {
$char = substr($string, 0, 1);
if ($char == $until) {
break;
}
$read .= $char;
$string = substr($string, 1);
}
return $read;
}
protected function doParseVar($var)
{
if($var == 'year')
{
return date('Y');
}
$val = $this->processVariableName($var);
return $val;
}
protected function doParseIf($condition, $stack)
{
if ($this->ifConditionIsTrue($condition)) {
return $this->processStack($stack);
} else {
return '';
}
}
protected function doParseIfNot($condition, $stack)
{
if (!$this->ifConditionIsTrue($condition)) {
return $this->processStack($stack);
} else {
return '';
}
}
protected function ifConditionIsTrue($condition)
{
$matches = array();
if (preg_match('/([a-zA-Z0-9_\-\(\):\s.\"]+)\s+?([\!\=\<\>]+)?\s+?([a-zA-Z0-9\(\)_\-:\s.\"]+)?/', $condition, $matches)) {
$left = is_numeric($matches[1]) ? intval($matches[1]) : $this->processVariableName($matches[1]);
$right = is_numeric($matches[3]) ? intval($matches[3]) : $this->processVariableName($matches[3]);
$operator = $matches[2];
switch ($operator) {
case '==':
case '=':
return ($left == $right);
case '!=':
return ($left != $right);
case '>=':
return ($left >= $right);
case '<=':
return ($left <= $right);
case '>':
return ($left > $right);
case '<':
return ($left < $right);
}
} elseif (preg_match('/([a-zA-Z0-9_\-\(\):\s.]+)/', $condition, $matches)) {
return $this->processVariableName($condition) ? true : false;
}
}
protected function doParseLoop($var, $stack)
{
$working = $this->processVariableName($var);
if(is_null($working))
{
return '';
}
if(!is_array($working))
{
$working = array($working);
}
$rtn = '';
foreach ($working as $key => $val) {
// Make sure we support nesting loops:
$keyWas = isset($this->key) ? $this->key : null;
$valWas = isset($this->value) ? $this->value : null;
$itemWas = isset($this->item) ? $this->item : null;
// Set up the necessary variables within the stack:
$this->parent = $this;
$this->item = $val;
$this->key = $key;
$this->value = $val;
$rtn .= $this->processStack($stack);
// Restore state for any parent nested loops:
$this->item = $itemWas;
$this->key = $keyWas;
$this->value = $valWas;
}
return $rtn;
}
/**
* Processes loops in templates, of the following styles:
*
* <code>
* {for myarray.items}
* {@item.title}
* {/for}
* </code>
*
* Or:
*
* <code>
* {for 0:pages.count; i++}
* <a href="/item/{@i}">{@i}</a>
* {/for}
* </code>
*
* @param $cond string The condition string for the loop, to be parsed (e.g. "myarray.items" or "0:pages.count; i++")
* @param $stack string The child stack for this loop, to be processed for each item.
* @return string
* @throws \Exception
*/
protected function doParseFor($cond, $stack)
{
// If this is a simple foreach loop, jump over to parse loop:
if (strpos($cond, ';') === false) {
return $this->doParseLoop($cond, $stack);
}
// Otherwise, process as a for loop:
$parts = explode(';', $cond);
$range = explode(':', trim($parts[0]));
// Process range:
$rangeLeft = $this->getForRangePart($range[0]);
$rangeRight = $this->getForRangePart($range[1]);
// Process variable & incrementor / decrementor:
$parts[1] = trim($parts[1]);
$matches = array();
if (preg_match('/([a-zA-Z0-9_]+)(\+\+|\-\-)/', $parts[1], $matches)) {
$varName = $matches[1];
$direction = $matches[2] == '++' ? 'increment' : 'decrement';
} else {
throw new \Exception('Syntax error in for loop: ' . $cond);
}
$rtn = '';
if ($direction == 'increment') {
for ($i = $rangeLeft; $i < $rangeRight; $i++) {
$this->parent = $this;
$this->{$varName} = $i;
$rtn .= $this->processStack($stack);
}
} else {
for ($i = $rangeLeft; $i > $rangeRight; $i--) {
$this->parent = $this;
$this->{$varName} = $i;
$rtn .= $this->processStack($stack);
}
}
return $rtn;
}
protected function getForRangePart($part)
{
if (is_numeric($part)) {
return intval($part);
}
$varPart = $this->processVariableName($part);
if (is_numeric($varPart)) {
return intval($varPart);
}
throw new \Exception('Invalid range in for loop: ' . $part);
}
public function processVariableName($varName)
{
// Case one - Test for function calls:
if (substr($varName, 0, 1) == '(' && substr($varName, -1) == ')') {
$functionCall = substr($varName, 1, -1);
$parts = explode(' ', $functionCall, 2);
$functionName = $parts[0];
$arguments = isset($parts[1]) ? $parts[1] : null;
return $this->executeTemplateFunction($functionName, $arguments);
}
// Case two - Test if it is just a string:
if (substr($varName, 0, 1) == '"' && substr($varName, -1) == '"') {
return substr($varName, 1, -1);
}
// Case three - Test if it is just a number:
if (is_numeric($varName)) {
return $varName;
}
// Case four - Test for helper calls:
if (strpos($varName, ':') !== false) {
list($helper, $property) = explode(':', $varName);
$helper = $this->{$helper}();
if (property_exists($helper, $property) || method_exists($helper, '__get')) {
return $helper->{$property};
}
return null;
}
// Case five - Process as a variable:
$varPart = explode('.', $varName);
$thisPart = array_shift($varPart);
if(!array_key_exists($thisPart, $this->_vars))
{
return null;
}
$working = $this->{$thisPart};
while(count($varPart))
{
$thisPart = array_shift($varPart);
if(is_object($working)) {
// Check if we're working with an actual property:
if (property_exists($working, $thisPart)) {
$working = $working->{$thisPart};
continue;
}
// Check if the object has a magic __get method:
if (method_exists($working, '__get')) {
$working = $working->{$thisPart};
continue;
}
}
if(is_array($working) && array_key_exists($thisPart, $working))
{
$working = $working[$thisPart];
continue;
}
if($thisPart == 'toLowerCase')
{
$working = strtolower($working);
continue;
}
if($thisPart == 'toUpperCase')
{
$working = strtoupper($working);
continue;
}
if ($thisPart == 'isNumeric')
{
return is_numeric($working);
}
return null;
}
return $working;
}
protected function doParseFunction($stack)
{
return $this->executeTemplateFunction($stack['function_name'], $stack['cond']);
}
protected function executeTemplateFunction($function, $args)
{
if (array_key_exists($function, self::$templateFunctions)) {
$handler = self::$templateFunctions[$function];
$args = $this->processFunctionArguments($args);
return $handler($args, $this);
}
return null;
}
protected function processFunctionArguments($args)
{
$rtn = array();
$args = explode(';', $args);
foreach ($args as $arg) {
$arg = explode(':', $arg);
if (count($arg) == 2) {
$key = trim($arg[0]);
$val = trim($arg[1]);
if (strpos($val, ',') !== false) {
$val = explode(',', $val);
}
$rtn[$key] = $val;
}
}
return $rtn;
}
public function getVariable($variable)
{
return $this->processVariableName($variable);
}
protected function includeTemplate($args, $view)
{
$template = static::createFromFile($view->getVariable($args['template']));
if (isset($args['variables'])) {
if (!is_array($args['variables'])) {
$args['variables'] = array($args['variables']);
}
foreach ($args['variables'] as $variable) {
$variable = explode('=>', $variable);
$variable = array_map('trim', $variable);
if (count($variable) == 1) {
$template->{$variable[0]} = $view->getVariable($variable[0]);
} else {
$template->{$variable[1]} = $view->getVariable($variable[0]);
}
}
}
return $template->render();
}
protected function callHelperFunction($args)
{
$helper = $args['helper'];
$function = $args['method'];
return $this->{$helper}()->{$function}();
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace b8\View;
use b8\View\Template;
class UserView extends Template
{
public function __construct($string)
{
trigger_error('Use of UserView is now deprecated. Please use Template instead.', E_USER_NOTICE);
parent::__construct($string);
}
}

174
src/PHPCI/Application.php Normal file
View file

@ -0,0 +1,174 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI;
use b8;
use b8\Exception\HttpException;
use b8\Http\Response;
use b8\Http\Response\RedirectResponse;
use b8\View;
/**
* PHPCI Front Controller
* @author Dan Cryer <dan@block8.co.uk>
*/
class Application extends b8\Application
{
/**
* @var \PHPCI\Controller
*/
protected $controller;
/**
* Initialise PHPCI - Handles session verification, routing, etc.
*/
public function init()
{
$request =& $this->request;
$route = '/:controller/:action';
$opts = array('controller' => 'Home', 'action' => 'index');
// Inlined as a closure to fix "using $this when not in object context" on 5.3
$validateSession = function () {
if (!empty($_SESSION['phpci_user_id'])) {
$user = b8\Store\Factory::getStore('User')->getByPrimaryKey($_SESSION['phpci_user_id']);
if ($user) {
$_SESSION['phpci_user'] = $user;
return true;
}
unset($_SESSION['phpci_user_id']);
}
return false;
};
$skipAuth = array($this, 'shouldSkipAuth');
// Handler for the route we're about to register, checks for a valid session where necessary:
$routeHandler = function (&$route, Response &$response) use (&$request, $validateSession, $skipAuth) {
$skipValidation = in_array($route['controller'], array('session', 'webhook', 'build-status'));
if (!$skipValidation && !$validateSession() && (!is_callable($skipAuth) || !$skipAuth())) {
if ($request->isAjax()) {
$response->setResponseCode(401);
$response->setContent('');
} else {
$_SESSION['phpci_login_redirect'] = substr($request->getPath(), 1);
$response = new RedirectResponse($response);
$response->setHeader('Location', PHPCI_URL.'session/login');
}
return false;
}
return true;
};
$this->router->clearRoutes();
$this->router->register($route, $opts, $routeHandler);
}
/**
* Handle an incoming web request.
*
* @return b8\b8\Http\Response|Response
*/
public function handleRequest()
{
try {
$this->response = parent::handleRequest();
} catch (HttpException $ex) {
$this->config->set('page_title', 'Error');
$view = new View('exception');
$view->exception = $ex;
$this->response->setResponseCode($ex->getErrorCode());
$this->response->setContent($view->render());
} catch (\Exception $ex) {
$this->config->set('page_title', 'Error');
$view = new View('exception');
$view->exception = $ex;
$this->response->setResponseCode(500);
$this->response->setContent($view->render());
}
if ($this->response->hasLayout() && $this->controller->layout) {
$this->setLayoutVariables($this->controller->layout);
$this->controller->layout->content = $this->response->getContent();
$this->response->setContent($this->controller->layout->render());
}
return $this->response;
}
/**
* Loads a particular controller, and injects our layout view into it.
* @param $class
* @return mixed
*/
protected function loadController($class)
{
$controller = parent::loadController($class);
$controller->layout = new View('layout');
$controller->layout->title = 'PHPCI';
$controller->layout->breadcrumb = array();
return $controller;
}
/**
* Injects variables into the layout before rendering it.
* @param View $layout
*/
protected function setLayoutVariables(View &$layout)
{
$groups = array();
$groupStore = b8\Store\Factory::getStore('ProjectGroup');
$groupList = $groupStore->getWhere(array(), 100, 0, array(), array('title' => 'ASC'));
foreach ($groupList['items'] as $group) {
$thisGroup = array('title' => $group->getTitle());
$projects = b8\Store\Factory::getStore('Project')->getByGroupId($group->getId());
$thisGroup['projects'] = $projects['items'];
$groups[] = $thisGroup;
}
$layout->groups = $groups;
}
/**
* Check whether we should skip auth (because it is disabled)
* @return bool
*/
protected function shouldSkipAuth()
{
$config = b8\Config::getInstance();
$state = (bool)$config->get('phpci.authentication_settings.state', false);
$userId = $config->get('phpci.authentication_settings.user_id', 0);
if (false !== $state && 0 != (int)$userId) {
$user = b8\Store\Factory::getStore('User')
->getByPrimaryKey($userId);
if ($user) {
$_SESSION['phpci_user'] = $user;
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,79 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI;
use b8\Store\Factory;
use PHPCI\Model\Build;
/**
* PHPCI Build Factory - Takes in a generic "Build" and returns a type-specific build model.
* @author Dan Cryer <dan@block8.co.uk>
*/
class BuildFactory
{
/**
* @param $buildId
* @return Build
* @throws \Exception
*/
public static function getBuildById($buildId)
{
$build = Factory::getStore('Build')->getById($buildId);
if (empty($build)) {
throw new \Exception('Build ID ' . $buildId . ' does not exist.');
}
return self::getBuild($build);
}
/**
* Takes a generic build and returns a type-specific build model.
* @param Build $build The build from which to get a more specific build type.
* @return Build
*/
public static function getBuild(Build $build)
{
$project = $build->getProject();
if (!empty($project)) {
switch ($project->getType()) {
case 'remote':
$type = 'RemoteGitBuild';
break;
case 'local':
$type = 'LocalBuild';
break;
case 'github':
$type = 'GithubBuild';
break;
case 'bitbucket':
$type = 'BitbucketBuild';
break;
case 'gitlab':
$type = 'GitlabBuild';
break;
case 'hg':
$type = 'MercurialBuild';
break;
case 'svn':
$type = 'SubversionBuild';
break;
default:
return $build;
}
$class = '\\PHPCI\\Model\\Build\\' . $type;
$build = new $class($build->getDataArray());
}
return $build;
}
}

423
src/PHPCI/Builder.php Normal file
View file

@ -0,0 +1,423 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI;
use PHPCI\Helper\BuildInterpolator;
use PHPCI\Helper\Lang;
use PHPCI\Helper\MailerFactory;
use PHPCI\Logging\BuildLogger;
use PHPCI\Model\Build;
use b8\Config;
use b8\Store\Factory;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use PHPCI\Plugin\Util\Factory as PluginFactory;
/**
* PHPCI Build Runner
* @author Dan Cryer <dan@block8.co.uk>
*/
class Builder implements LoggerAwareInterface
{
/**
* @var string
*/
public $buildPath;
/**
* @var string[]
*/
public $ignore = array();
/**
* @var string
*/
protected $directory;
/**
* @var bool
*/
protected $verbose = true;
/**
* @var \PHPCI\Model\Build
*/
protected $build;
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @var array
*/
protected $config;
/**
* @var string
*/
protected $lastOutput;
/**
* @var BuildInterpolator
*/
protected $interpolator;
/**
* @var \PHPCI\Store\BuildStore
*/
protected $store;
/**
* @var bool
*/
public $quiet = false;
/**
* @var \PHPCI\Plugin\Util\Executor
*/
protected $pluginExecutor;
/**
* @var Helper\CommandExecutor
*/
protected $commandExecutor;
/**
* @var Logging\BuildLogger
*/
protected $buildLogger;
/**
* Set up the builder.
* @param \PHPCI\Model\Build $build
* @param LoggerInterface $logger
*/
public function __construct(Build $build, LoggerInterface $logger = null)
{
$this->build = $build;
$this->store = Factory::getStore('Build');
$this->buildLogger = new BuildLogger($logger, $build);
$pluginFactory = $this->buildPluginFactory($build);
$pluginFactory->addConfigFromFile(PHPCI_DIR . "/pluginconfig.php");
$this->pluginExecutor = new Plugin\Util\Executor($pluginFactory, $this->buildLogger);
$executorClass = 'PHPCI\Helper\UnixCommandExecutor';
if (IS_WIN) {
$executorClass = 'PHPCI\Helper\WindowsCommandExecutor';
}
$this->commandExecutor = new $executorClass(
$this->buildLogger,
PHPCI_DIR,
$this->quiet,
$this->verbose
);
$this->interpolator = new BuildInterpolator();
}
/**
* Set the config array, as read from phpci.yml
* @param array|null $config
* @throws \Exception
*/
public function setConfigArray($config)
{
if (is_null($config) || !is_array($config)) {
throw new \Exception(Lang::get('missing_phpci_yml'));
}
$this->config = $config;
}
/**
* Access a variable from the phpci.yml file.
* @param string
* @return mixed
*/
public function getConfig($key)
{
$rtn = null;
if (isset($this->config[$key])) {
$rtn = $this->config[$key];
}
return $rtn;
}
/**
* Access a variable from the config.yml
* @param $key
* @return mixed
*/
public function getSystemConfig($key)
{
return Config::getInstance()->get($key);
}
/**
* @return string The title of the project being built.
*/
public function getBuildProjectTitle()
{
return $this->build->getProject()->getTitle();
}
/**
* Run the active build.
*/
public function execute()
{
// Update the build in the database, ping any external services.
$this->build->setStatus(Build::STATUS_RUNNING);
$this->build->setStarted(new \DateTime());
$this->store->save($this->build);
$this->build->sendStatusPostback();
$success = true;
$previous_build = $this->build->getProject()->getPreviousBuild($this->build->getBranch());
$previous_state = Build::STATUS_NEW;
if ($previous_build) {
$previous_state = $previous_build->getStatus();
}
try {
// Set up the build:
$this->setupBuild();
// Run the core plugin stages:
foreach (array('setup', 'test') as $stage) {
$success &= $this->pluginExecutor->executePlugins($this->config, $stage);
}
// Set the status so this can be used by complete, success and failure
// stages.
if ($success) {
$this->build->setStatus(Build::STATUS_SUCCESS);
} else {
$this->build->setStatus(Build::STATUS_FAILED);
}
if ($success) {
$this->pluginExecutor->executePlugins($this->config, 'success');
if ($previous_state == Build::STATUS_FAILED) {
$this->pluginExecutor->executePlugins($this->config, 'fixed');
}
$this->buildLogger->logSuccess(Lang::get('build_success'));
} else {
$this->pluginExecutor->executePlugins($this->config, 'failure');
if ($previous_state == Build::STATUS_SUCCESS || $previous_state == Build::STATUS_NEW) {
$this->pluginExecutor->executePlugins($this->config, 'broken');
}
$this->buildLogger->logFailure(Lang::get('build_failed'));
}
} catch (\Exception $ex) {
$this->build->setStatus(Build::STATUS_FAILED);
$this->buildLogger->logFailure(Lang::get('exception') . $ex->getMessage());
}finally{
// Complete stage plugins are always run
$this->pluginExecutor->executePlugins($this->config, 'complete');
}
// Update the build in the database, ping any external services, etc.
$this->build->sendStatusPostback();
$this->build->setFinished(new \DateTime());
// Clean up:
$this->buildLogger->log(Lang::get('removing_build'));
$this->build->removeBuildDirectory();
$this->store->save($this->build);
}
/**
* Used by this class, and plugins, to execute shell commands.
*/
public function executeCommand()
{
return $this->commandExecutor->executeCommand(func_get_args());
}
/**
* Returns the output from the last command run.
*/
public function getLastOutput()
{
return $this->commandExecutor->getLastOutput();
}
/**
* Specify whether exec output should be logged.
* @param bool $enableLog
*/
public function logExecOutput($enableLog = true)
{
$this->commandExecutor->logExecOutput = $enableLog;
}
/**
* Find a binary required by a plugin.
* @param string $binary
* @param bool $quiet
*
* @return null|string
*/
public function findBinary($binary, $quiet = false)
{
return $this->commandExecutor->findBinary($binary, $quiet = false);
}
/**
* Replace every occurrence of the interpolation vars in the given string
* Example: "This is build %PHPCI_BUILD%" => "This is build 182"
* @param string $input
* @return string
*/
public function interpolate($input)
{
return $this->interpolator->interpolate($input);
}
/**
* Set up a working copy of the project for building.
*/
protected function setupBuild()
{
$this->buildPath = $this->build->getBuildPath();
$this->interpolator->setupInterpolationVars(
$this->build,
$this->buildPath,
PHPCI_URL
);
$this->commandExecutor->setBuildPath($this->buildPath);
// Create a working copy of the project:
if (!$this->build->createWorkingCopy($this, $this->buildPath)) {
throw new \Exception(Lang::get('could_not_create_working'));
}
// Does the project's phpci.yml request verbose mode?
if (!isset($this->config['build_settings']['verbose']) || !$this->config['build_settings']['verbose']) {
$this->verbose = false;
}
// Does the project have any paths it wants plugins to ignore?
if (isset($this->config['build_settings']['ignore'])) {
$this->ignore = $this->config['build_settings']['ignore'];
}
$this->buildLogger->logSuccess(Lang::get('working_copy_created', $this->buildPath));
return true;
}
/**
* Sets a logger instance on the object
*
* @param LoggerInterface $logger
* @return null
*/
public function setLogger(LoggerInterface $logger)
{
$this->buildLogger->setLogger($logger);
}
/**
* Write to the build log.
* @param $message
* @param string $level
* @param array $context
*/
public function log($message, $level = LogLevel::INFO, $context = array())
{
$this->buildLogger->log($message, $level, $context);
}
/**
* Add a success-coloured message to the log.
* @param string
*/
public function logSuccess($message)
{
$this->buildLogger->logSuccess($message);
}
/**
* Add a failure-coloured message to the log.
* @param string $message
* @param \Exception $exception The exception that caused the error.
*/
public function logFailure($message, \Exception $exception = null)
{
$this->buildLogger->logFailure($message, $exception);
}
/**
* Returns a configured instance of the plugin factory.
*
* @param Build $build
* @return PluginFactory
*/
private function buildPluginFactory(Build $build)
{
$pluginFactory = new PluginFactory();
$self = $this;
$pluginFactory->registerResource(
function () use ($self) {
return $self;
},
null,
'PHPCI\Builder'
);
$pluginFactory->registerResource(
function () use ($build) {
return $build;
},
null,
'PHPCI\Model\Build'
);
$logger = $this->logger;
$pluginFactory->registerResource(
function () use ($logger) {
return $logger;
},
null,
'Psr\Log\LoggerInterface'
);
$pluginFactory->registerResource(
function () use ($self) {
$factory = new MailerFactory($self->getSystemConfig('phpci'));
return $factory->getSwiftMailerFromConfig();
},
null,
'Swift_Mailer'
);
return $pluginFactory;
}
}

View file

@ -0,0 +1,82 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use PHPCI\Service\UserService;
use PHPCI\Helper\Lang;
use PHPCI\Store\UserStore;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Create admin command - creates an admin user
* @author Wogan May (@woganmay)
* @package PHPCI
* @subpackage Console
*/
class CreateAdminCommand extends Command
{
/**
* @var UserStore
*/
protected $userStore;
/**
* @param UserStore $userStore
*/
public function __construct(UserStore $userStore)
{
parent::__construct();
$this->userStore = $userStore;
}
protected function configure()
{
$this
->setName('phpci:create-admin')
->setDescription(Lang::get('create_admin_user'));
}
/**
* Creates an admin user in the existing PHPCI database
*
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$userService = new UserService($this->userStore);
/** @var $dialog \Symfony\Component\Console\Helper\DialogHelper */
$dialog = $this->getHelperSet()->get('dialog');
// Function to validate mail address.
$mailValidator = function ($answer) {
if (!filter_var($answer, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException(Lang::get('must_be_valid_email'));
}
return $answer;
};
$adminEmail = $dialog->askAndValidate($output, Lang::get('enter_email'), $mailValidator, false);
$adminName = $dialog->ask($output, Lang::get('enter_name'));
$adminPass = $dialog->askHiddenResponse($output, Lang::get('enter_password'));
try {
$userService->createUser($adminName, $adminEmail, $adminPass, true);
$output->writeln(Lang::get('user_created'));
} catch (\Exception $e) {
$output->writeln(sprintf('<error>%s</error>', Lang::get('failed_to_create')));
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
}
}
}

View file

@ -0,0 +1,85 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use PHPCI\Helper\Lang;
use PHPCI\Service\BuildService;
use PHPCI\Store\ProjectStore;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Create build command - creates a build for a project
* @author Jérémy DECOOL (@jdecool)
* @package PHPCI
* @subpackage Console
*/
class CreateBuildCommand extends Command
{
/**
* @var ProjectStore
*/
protected $projectStore;
/**
* @var BuildService
*/
protected $buildService;
/**
* @param ProjectStore $projectStore
*/
public function __construct(ProjectStore $projectStore, BuildService $buildService)
{
parent::__construct();
$this->projectStore = $projectStore;
$this->buildService = $buildService;
}
/**
* {@inheritDoc}
*/
protected function configure()
{
$this
->setName('phpci:create-build')
->setDescription(Lang::get('create_build_project'))
->addArgument('projectId', InputArgument::REQUIRED, Lang::get('project_id_argument'))
->addOption('commit', null, InputOption::VALUE_OPTIONAL, Lang::get('commit_id_option'))
->addOption('branch', null, InputOption::VALUE_OPTIONAL, Lang::get('branch_name_option'));
}
/**
* {@inheritDoc}
*/
public function execute(InputInterface $input, OutputInterface $output)
{
$projectId = $input->getArgument('projectId');
$commitId = $input->getOption('commit');
$branch = $input->getOption('branch');
$project = $this->projectStore->getById($projectId);
if (empty($project)) {
throw new \InvalidArgumentException('Project does not exist: ' . $projectId);
}
try {
$this->buildService->createBuild($project, $commitId, $branch);
$output->writeln(Lang::get('build_created'));
} catch (\Exception $e) {
$output->writeln(sprintf('<error>%s</error>', Lang::get('failed')));
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
}
}
}

View file

@ -0,0 +1,202 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use Monolog\Logger;
use PHPCI\ProcessControl\Factory;
use PHPCI\ProcessControl\ProcessControlInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Daemon that loops and call the run-command.
* @author Gabriel Baker <gabriel.baker@autonomicpilot.co.uk>
* @package PHPCI
* @subpackage Console
*/
class DaemonCommand extends Command
{
/**
* @var Logger
*/
protected $logger;
/**
* @var string
*/
protected $pidFilePath;
/**
* @var string
*/
protected $logFilePath;
/**
* @var ProcessControlInterface
*/
protected $processControl;
public function __construct(Logger $logger, ProcessControlInterface $processControl = null, $name = null)
{
parent::__construct($name);
$this->logger = $logger;
$this->processControl = $processControl ?: Factory::getInstance();
}
protected function configure()
{
$this
->setName('phpci:daemon')
->setDescription('Initiates the daemon to run commands.')
->addArgument(
'state', InputArgument::REQUIRED, 'start|stop|status'
)
->addOption(
'pid-file', 'p', InputOption::VALUE_REQUIRED,
'Path of the PID file',
implode(DIRECTORY_SEPARATOR,
array(PHPCI_DIR, 'daemon', 'daemon.pid'))
)
->addOption(
'log-file', 'l', InputOption::VALUE_REQUIRED,
'Path of the log file',
implode(DIRECTORY_SEPARATOR,
array(PHPCI_DIR, 'daemon', 'daemon.log'))
);
}
/**
* Loops through running.
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->pidFilePath = $input->getOption('pid-file');
$this->logFilePath = $input->getOption('log-file');
$state = $input->getArgument('state');
switch ($state) {
case 'start':
$this->startDaemon();
break;
case 'stop':
$this->stopDaemon();
break;
case 'status':
$this->statusDaemon($output);
break;
default:
$this->output->writeln("<error>Not a valid choice, please use start, stop or status</error>");
break;
}
}
protected function startDaemon()
{
$pid = $this->getRunningPid();
if ($pid) {
$this->logger->notice("Daemon already started", array('pid' => $pid));
return "alreadystarted";
}
$this->logger->info("Trying to start the daemon");
$cmd = "nohup %s/daemonise phpci:daemonise > %s 2>&1 &";
$command = sprintf($cmd, PHPCI_DIR, $this->logFilePath);
$output = $exitCode = null;
exec($command, $output, $exitCode);
if ($exitCode !== 0) {
$this->logger->error(sprintf("daemonise exited with status %d", $exitCode));
return "notstarted";
}
for ($i = 0; !($pid = $this->getRunningPid()) && $i < 5; $i++) {
sleep(1);
}
if (!$pid) {
$this->logger->error("Could not start the daemon");
return "notstarted";
}
$this->logger->notice("Daemon started", array('pid' => $pid));
return "started";
}
protected function stopDaemon()
{
$pid = $this->getRunningPid();
if (!$pid) {
$this->logger->notice("Cannot stop the daemon as it is not started");
return "notstarted";
}
$this->logger->info("Trying to terminate the daemon", array('pid' => $pid));
$this->processControl->kill($pid);
for ($i = 0; ($pid = $this->getRunningPid()) && $i < 5; $i++) {
sleep(1);
}
if ($pid) {
$this->logger->warning("The daemon is resiting, trying to kill it", array('pid' => $pid));
$this->processControl->kill($pid, true);
for ($i = 0; ($pid = $this->getRunningPid()) && $i < 5; $i++) {
sleep(1);
}
}
if (!$pid) {
$this->logger->notice("Daemon stopped");
return "stopped";
}
$this->logger->error("Could not stop the daemon");
}
protected function statusDaemon(OutputInterface $output)
{
$pid = $this->getRunningPid();
if ($pid) {
$output->writeln(sprintf('The daemon is running, PID: %d', $pid));
return "running";
}
$output->writeln('The daemon is not running');
return "notrunning";
}
/** Check if there is a running daemon
*
* @return int|null
*/
protected function getRunningPid()
{
if (!file_exists($this->pidFilePath)) {
return;
}
$pid = intval(trim(file_get_contents($this->pidFilePath)));
if($this->processControl->isRunning($pid, true)) {
return $pid;
}
// Not found, remove the stale PID file
unlink($this->pidFilePath);
}
}

View file

@ -0,0 +1,110 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use Monolog\Logger;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Daemon that loops and call the run-command.
* @author Gabriel Baker <gabriel.baker@autonomicpilot.co.uk>
* @package PHPCI
* @subpackage Console
*/
class DaemoniseCommand extends Command
{
/**
* @var Logger
*/
protected $logger;
/**
* @var OutputInterface
*/
protected $output;
/**
* @var boolean
*/
protected $run;
/**
* @var int
*/
protected $sleep;
/**
* @param \Monolog\Logger $logger
* @param string $name
*/
public function __construct(Logger $logger, $name = null)
{
parent::__construct($name);
$this->logger = $logger;
}
protected function configure()
{
$this
->setName('phpci:daemonise')
->setDescription('Starts the daemon to run commands.');
}
/**
* Loops through running.
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$cmd = "echo %s > '%s/daemon/daemon.pid'";
$command = sprintf($cmd, getmypid(), PHPCI_DIR);
exec($command);
$this->output = $output;
$this->run = true;
$this->sleep = 0;
$runner = new RunCommand($this->logger);
$runner->setMaxBuilds(1);
$runner->setDaemon(true);
$emptyInput = new ArgvInput(array());
while ($this->run) {
$buildCount = 0;
try {
$buildCount = $runner->run($emptyInput, $output);
} catch (\Exception $e) {
$output->writeln('<error>Exception: ' . $e->getMessage() . '</error>');
$output->writeln('<error>Line: ' . $e->getLine() . ' - File: ' . $e->getFile() . '</error>');
}
if (0 == $buildCount && $this->sleep < 15) {
$this->sleep++;
} elseif (1 < $this->sleep) {
$this->sleep--;
}
echo '.'.(0 === $buildCount?'':'build');
sleep($this->sleep);
}
}
/**
* Called when log entries are made in Builder / the plugins.
* @see \PHPCI\Builder::log()
*/
public function logCallback($log)
{
$this->output->writeln($log);
}
}

View file

@ -0,0 +1,48 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use b8\Database;
use b8\Database\CodeGenerator;
/**
* Generate console command - Reads the database and generates models and stores.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Console
*/
class GenerateCommand extends Command
{
protected function configure()
{
$this
->setName('phpci:generate')
->setDescription('Generate models and stores from the database.');
}
/**
* Generates Model and Store classes by reading database meta data.
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$gen = new CodeGenerator(
Database::getConnection(),
array('default' => 'PHPCI'),
array('default' => PHPCI_DIR),
false
);
$gen->generateModels();
$gen->generateStores();
}
}

View file

@ -0,0 +1,422 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use Exception;
use PDO;
use b8\Config;
use b8\Store\Factory;
use PHPCI\Helper\Lang;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\DialogHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use PHPCI\Service\UserService;
use Symfony\Component\Console\Question\ConfirmationQuestion;
/**
* Install console command - Installs PHPCI.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Console
*/
class InstallCommand extends Command
{
protected $configFilePath;
protected function configure()
{
$defaultPath = PHPCI_DIR . 'PHPCI/config.yml';
$this
->setName('phpci:install')
->addOption('url', null, InputOption::VALUE_OPTIONAL, Lang::get('installation_url'))
->addOption('db-host', null, InputOption::VALUE_OPTIONAL, Lang::get('db_host'))
->addOption('db-name', null, InputOption::VALUE_OPTIONAL, Lang::get('db_name'))
->addOption('db-user', null, InputOption::VALUE_OPTIONAL, Lang::get('db_user'))
->addOption('db-pass', null, InputOption::VALUE_OPTIONAL, Lang::get('db_pass'))
->addOption('admin-name', null, InputOption::VALUE_OPTIONAL, Lang::get('admin_name'))
->addOption('admin-pass', null, InputOption::VALUE_OPTIONAL, Lang::get('admin_pass'))
->addOption('admin-mail', null, InputOption::VALUE_OPTIONAL, Lang::get('admin_email'))
->addOption('config-path', null, InputOption::VALUE_OPTIONAL, Lang::get('config_path'), $defaultPath)
->addOption('queue-disabled', null, InputOption::VALUE_NONE, 'Don\'t ask for queue details')
->addOption('queue-server', null, InputOption::VALUE_OPTIONAL, 'Beanstalkd queue server hostname')
->addOption('queue-name', null, InputOption::VALUE_OPTIONAL, 'Beanstalkd queue name')
->setDescription(Lang::get('install_phpci'));
}
/**
* Installs PHPCI - Can be run more than once as long as you ^C instead of entering an email address.
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->configFilePath = $input->getOption('config-path');
if (!$this->verifyNotInstalled($output)) {
return;
}
$output->writeln('');
$output->writeln('<info>******************</info>');
$output->writeln('<info> '.Lang::get('welcome_to_phpci').'</info>');
$output->writeln('<info>******************</info>');
$output->writeln('');
$this->checkRequirements($output);
$output->writeln(Lang::get('please_answer'));
$output->writeln('-------------------------------------');
$output->writeln('');
// ----
// Get MySQL connection information and verify that it works:
// ----
$connectionVerified = false;
while (!$connectionVerified) {
$db = $this->getDatabaseInformation($input, $output);
$connectionVerified = $this->verifyDatabaseDetails($db, $output);
}
$output->writeln('');
$conf = array();
$conf['b8']['database'] = $db;
// ----
// Get basic installation details (URL, etc)
// ----
$conf['phpci'] = $this->getPhpciConfigInformation($input, $output);
$this->writeConfigFile($conf);
$this->setupDatabase($output);
$admin = $this->getAdminInformation($input, $output);
$this->createAdminUser($admin, $output);
}
/**
* Check PHP version, required modules and for disabled functions.
*
* @param OutputInterface $output
* @throws \Exception
*/
protected function checkRequirements(OutputInterface $output)
{
$output->write('Checking requirements...');
$errors = false;
// Check PHP version:
if (!(version_compare(PHP_VERSION, '5.3.8') >= 0)) {
$output->writeln('');
$output->writeln('<error>'.Lang::get('phpci_php_req').'</error>');
$errors = true;
}
// Check required extensions are present:
$requiredExtensions = array('PDO', 'pdo_mysql');
foreach ($requiredExtensions as $extension) {
if (!extension_loaded($extension)) {
$output->writeln('');
$output->writeln('<error>'.Lang::get('extension_required', $extension).'</error>');
$errors = true;
}
}
// Check required functions are callable:
$requiredFunctions = array('exec', 'shell_exec');
foreach ($requiredFunctions as $function) {
if (!function_exists($function)) {
$output->writeln('');
$output->writeln('<error>'.Lang::get('function_required', $function).'</error>');
$errors = true;
}
}
if (!function_exists('password_hash')) {
$output->writeln('');
$output->writeln('<error>'.Lang::get('function_required', $function).'</error>');
$errors = true;
}
if ($errors) {
throw new Exception(Lang::get('requirements_not_met'));
}
$output->writeln(' <info>'.Lang::get('ok').'</info>');
$output->writeln('');
}
/**
* Load information for admin user form CLI options or ask info to user.
*
* @param InputInterface $input
* @param OutputInterface $output
* @return array
*/
protected function getAdminInformation(InputInterface $input, OutputInterface $output)
{
$admin = array();
/**
* @var \Symfony\Component\Console\Helper\DialogHelper
*/
$dialog = $this->getHelperSet()->get('dialog');
// Function to validate mail address.
$mailValidator = function ($answer) {
if (!filter_var($answer, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException(Lang::get('must_be_valid_email'));
}
return $answer;
};
if ($adminEmail = $input->getOption('admin-mail')) {
$adminEmail = $mailValidator($adminEmail);
} else {
$adminEmail = $dialog->askAndValidate($output, Lang::get('enter_email'), $mailValidator, false);
}
if (!$adminName = $input->getOption('admin-name')) {
$adminName = $dialog->ask($output, Lang::get('enter_name'));
}
if (!$adminPass = $input->getOption('admin-pass')) {
$adminPass = $dialog->askHiddenResponse($output, Lang::get('enter_password'));
}
$admin['mail'] = $adminEmail;
$admin['name'] = $adminName;
$admin['pass'] = $adminPass;
return $admin;
}
/**
* Load configuration for PHPCI form CLI options or ask info to user.
*
* @param InputInterface $input
* @param OutputInterface $output
* @return array
*/
protected function getPhpciConfigInformation(InputInterface $input, OutputInterface $output)
{
$phpci = array();
/**
* @var \Symfony\Component\Console\Helper\DialogHelper
*/
$dialog = $this->getHelperSet()->get('dialog');
// FUnction do validate URL.
$urlValidator = function ($answer) {
if (!filter_var($answer, FILTER_VALIDATE_URL)) {
throw new Exception(Lang::get('must_be_valid_url'));
}
return rtrim($answer, '/');
};
if ($url = $input->getOption('url')) {
$url = $urlValidator($url);
} else {
$url = $dialog->askAndValidate($output, Lang::get('enter_phpci_url'), $urlValidator, false);
}
$phpci['url'] = $url;
$phpci['worker'] = $this->getQueueInformation($input, $output, $dialog);
return $phpci;
}
/**
* If the user wants to use a queue, get the necessary details.
* @param InputInterface $input
* @param OutputInterface $output
* @param DialogHelper $dialog
* @return array
*/
protected function getQueueInformation(InputInterface $input, OutputInterface $output, DialogHelper $dialog)
{
if ($input->getOption('queue-disabled')) {
return null;
}
$rtn = [];
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('Use beanstalkd to manage build queue? ', true);
if (!$helper->ask($input, $output, $question)) {
$output->writeln('<error>Skipping beanstalkd configuration.</error>');
return null;
}
if (!$rtn['host'] = $input->getOption('queue-server')) {
$rtn['host'] = $dialog->ask($output, 'Enter your beanstalkd hostname [localhost]: ', 'localhost');
}
if (!$rtn['queue'] = $input->getOption('queue-name')) {
$rtn['queue'] = $dialog->ask($output, 'Enter the queue (tube) name to use [phpci]: ', 'phpci');
}
return $rtn;
}
/**
* Load configuration for DB form CLI options or ask info to user.
*
* @param InputInterface $input
* @param OutputInterface $output
* @return array
*/
protected function getDatabaseInformation(InputInterface $input, OutputInterface $output)
{
$db = array();
/**
* @var \Symfony\Component\Console\Helper\DialogHelper
*/
$dialog = $this->getHelperSet()->get('dialog');
if (!$dbHost = $input->getOption('db-host')) {
$dbHost = $dialog->ask($output, Lang::get('enter_db_host'), 'localhost');
}
if (!$dbName = $input->getOption('db-name')) {
$dbName = $dialog->ask($output, Lang::get('enter_db_name'), 'phpci');
}
if (!$dbUser = $input->getOption('db-user')) {
$dbUser = $dialog->ask($output, Lang::get('enter_db_user'), 'phpci');
}
if (!$dbPass = $input->getOption('db-pass')) {
$dbPass = $dialog->askHiddenResponse($output, Lang::get('enter_db_pass'));
}
$db['servers']['read'] = $dbHost;
$db['servers']['write'] = $dbHost;
$db['name'] = $dbName;
$db['username'] = $dbUser;
$db['password'] = $dbPass;
return $db;
}
/**
* Try and connect to MySQL using the details provided.
* @param array $db
* @param OutputInterface $output
* @return bool
*/
protected function verifyDatabaseDetails(array $db, OutputInterface $output)
{
try {
$pdo = new PDO(
'mysql:host='.$db['servers']['write'].';dbname='.$db['name'],
$db['username'],
$db['password'],
array(
\PDO::ATTR_PERSISTENT => false,
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_TIMEOUT => 2,
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'',
)
);
unset($pdo);
return true;
} catch (Exception $ex) {
$output->writeln('<error>'.Lang::get('could_not_connect').'</error>');
$output->writeln('<error>' . $ex->getMessage() . '</error>');
}
return false;
}
/**
* Write the PHPCI config.yml file.
* @param array $config
*/
protected function writeConfigFile(array $config)
{
$dumper = new \Symfony\Component\Yaml\Dumper();
$yaml = $dumper->dump($config, 4);
file_put_contents($this->configFilePath, $yaml);
}
protected function setupDatabase(OutputInterface $output)
{
$output->write(Lang::get('setting_up_db'));
$phinxBinary = escapeshellarg(PHPCI_DIR . 'vendor/bin/phinx');
$phinxScript = escapeshellarg(PHPCI_DIR . 'phinx.php');
shell_exec($phinxBinary . ' migrate -c ' . $phinxScript);
$output->writeln('<info>'.Lang::get('ok').'</info>');
}
/**
* Create admin user using information loaded before.
*
* @param array $admin
* @param OutputInterface $output
*/
protected function createAdminUser($admin, $output)
{
try {
$this->reloadConfig();
$userStore = Factory::getStore('User');
$userService = new UserService($userStore);
$userService->createUser($admin['name'], $admin['mail'], $admin['pass'], 1);
$output->writeln('<info>'.Lang::get('user_created').'</info>');
} catch (\Exception $ex) {
$output->writeln('<error>'.Lang::get('failed_to_create').'</error>');
$output->writeln('<error>' . $ex->getMessage() . '</error>');
}
}
protected function reloadConfig()
{
$config = Config::getInstance();
if (file_exists($this->configFilePath)) {
$config->loadYaml($this->configFilePath);
}
}
/**
* @param OutputInterface $output
* @return bool
*/
protected function verifyNotInstalled(OutputInterface $output)
{
if (file_exists($this->configFilePath)) {
$content = file_get_contents($this->configFilePath);
if (!empty($content)) {
$output->writeln('<error>'.Lang::get('config_exists').'</error>');
$output->writeln('<error>'.Lang::get('update_instead').'</error>');
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,105 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use b8\Store\Factory;
use b8\HttpClient;
use Monolog\Logger;
use PHPCI\Helper\Lang;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Parser;
use PHPCI\Model\Build;
/**
* Run console command - Poll github for latest commit id
* @author Jimmy Cleuren <jimmy.cleuren@gmail.com>
* @package PHPCI
* @subpackage Console
*/
class PollCommand extends Command
{
/**
* @var \Monolog\Logger
*/
protected $logger;
public function __construct(Logger $logger, $name = null)
{
parent::__construct($name);
$this->logger = $logger;
}
protected function configure()
{
$this
->setName('phpci:poll-github')
->setDescription(Lang::get('poll_github'));
}
/**
* Pulls all pending builds from the database and runs them.
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$parser = new Parser();
$yaml = file_get_contents(APPLICATION_PATH . 'PHPCI/config.yml');
$this->settings = $parser->parse($yaml);
$token = $this->settings['phpci']['github']['token'];
if (!$token) {
$this->logger->error(Lang::get('no_token'));
return;
}
$buildStore = Factory::getStore('Build');
$this->logger->addInfo(Lang::get('finding_projects'));
$projectStore = Factory::getStore('Project');
$result = $projectStore->getWhere();
$this->logger->addInfo(Lang::get('found_n_projects', count($result['items'])));
foreach ($result['items'] as $project) {
$http = new HttpClient('https://api.github.com');
$commits = $http->get('/repos/' . $project->getReference() . '/commits', array('access_token' => $token));
$last_commit = $commits['body'][0]['sha'];
$last_committer = $commits['body'][0]['commit']['committer']['email'];
$message = $commits['body'][0]['commit']['message'];
$this->logger->info(Lang::get('last_commit_is', $project->getTitle(), $last_commit));
if ($project->getLastCommit() != $last_commit && $last_commit != "") {
$this->logger->info(
Lang::get('adding_new_build')
);
$build = new Build();
$build->setProjectId($project->getId());
$build->setCommitId($last_commit);
$build->setStatus(Build::STATUS_NEW);
$build->setBranch($project->getBranch());
$build->setCreated(new \DateTime());
$build->setCommitMessage($message);
if (!empty($last_committer)) {
$build->setCommitterEmail($last_committer);
}
$buildStore->save($build);
$project->setLastCommit($last_commit);
$projectStore->save($project);
}
}
$this->logger->addInfo(Lang::get('finished_processing_builds'));
}
}

View file

@ -0,0 +1,93 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use b8\Store\Factory;
use Monolog\Logger;
use PHPCI\Service\BuildService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Re-runs the last run build.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Console
*/
class RebuildCommand extends Command
{
/**
* @var Logger
*/
protected $logger;
/**
* @var OutputInterface
*/
protected $output;
/**
* @var boolean
*/
protected $run;
/**
* @var int
*/
protected $sleep;
/**
* @param \Monolog\Logger $logger
* @param string $name
*/
public function __construct(Logger $logger, $name = null)
{
parent::__construct($name);
$this->logger = $logger;
}
protected function configure()
{
$this
->setName('phpci:rebuild')
->setDescription('Re-runs the last run build.');
}
/**
* Loops through running.
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$runner = new RunCommand($this->logger);
$runner->setMaxBuilds(1);
$runner->setDaemon(false);
/** @var \PHPCI\Store\BuildStore $store */
$store = Factory::getStore('Build');
$service = new BuildService($store);
$builds = $store->getLatestBuilds(null, 1);
$lastBuild = array_shift($builds);
$service->createDuplicateBuild($lastBuild);
$runner->run(new ArgvInput(array()), $output);
}
/**
* Called when log entries are made in Builder / the plugins.
* @see \PHPCI\Builder::log()
*/
public function logCallback($log)
{
$this->output->writeln($log);
}
}

View file

@ -0,0 +1,85 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2015, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use b8\Config;
use b8\Store\Factory;
use Monolog\Logger;
use PHPCI\BuildFactory;
use PHPCI\Helper\Lang;
use PHPCI\Logging\OutputLogHandler;
use PHPCI\Service\BuildService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Console
*/
class RebuildQueueCommand extends Command
{
/**
* @var OutputInterface
*/
protected $output;
/**
* @var Logger
*/
protected $logger;
/**
* @param \Monolog\Logger $logger
* @param string $name
*/
public function __construct(Logger $logger, $name = null)
{
parent::__construct($name);
$this->logger = $logger;
}
protected function configure()
{
$this
->setName('phpci:rebuild-queue')
->setDescription('Rebuilds the PHPCI worker queue.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->output = $output;
// For verbose mode we want to output all informational and above
// messages to the symphony output interface.
if ($input->hasOption('verbose') && $input->getOption('verbose')) {
$this->logger->pushHandler(
new OutputLogHandler($this->output, Logger::INFO)
);
}
$store = Factory::getStore('Build');
$result = $store->getByStatus(0);
$this->logger->addInfo(Lang::get('found_n_builds', count($result['items'])));
$buildService = new BuildService($store);
while (count($result['items'])) {
$build = array_shift($result['items']);
$build = BuildFactory::getBuild($build);
$this->logger->addInfo('Added build #' . $build->getId() . ' to queue.');
$buildService->addBuildToQueue($build);
}
}
}

View file

@ -0,0 +1,178 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use b8\Config;
use Monolog\Logger;
use PHPCI\Helper\Lang;
use PHPCI\Logging\BuildDBLogHandler;
use PHPCI\Logging\LoggedBuildContextTidier;
use PHPCI\Logging\OutputLogHandler;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use b8\Store\Factory;
use PHPCI\Builder;
use PHPCI\BuildFactory;
use PHPCI\Model\Build;
/**
* Run console command - Runs any pending builds.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Console
*/
class RunCommand extends Command
{
/**
* @var OutputInterface
*/
protected $output;
/**
* @var Logger
*/
protected $logger;
/**
* @var int
*/
protected $maxBuilds = 100;
/**
* @var bool
*/
protected $isFromDaemon = false;
/**
* @param \Monolog\Logger $logger
* @param string $name
*/
public function __construct(Logger $logger, $name = null)
{
parent::__construct($name);
$this->logger = $logger;
}
protected function configure()
{
$this
->setName('phpci:run-builds')
->setDescription(Lang::get('run_all_pending'));
}
/**
* Pulls all pending builds from the database and runs them.
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->output = $output;
// For verbose mode we want to output all informational and above
// messages to the symphony output interface.
if ($input->hasOption('verbose') && $input->getOption('verbose')) {
$this->logger->pushHandler(
new OutputLogHandler($this->output, Logger::INFO)
);
}
$running = $this->validateRunningBuilds();
$this->logger->pushProcessor(new LoggedBuildContextTidier());
$this->logger->addInfo(Lang::get('finding_builds'));
$store = Factory::getStore('Build');
$result = $store->getByStatus(0, $this->maxBuilds);
$this->logger->addInfo(Lang::get('found_n_builds', count($result['items'])));
$builds = 0;
while (count($result['items'])) {
$build = array_shift($result['items']);
$build = BuildFactory::getBuild($build);
// Skip build (for now) if there's already a build running in that project:
if (!$this->isFromDaemon && in_array($build->getProjectId(), $running)) {
$this->logger->addInfo(Lang::get('skipping_build', $build->getId()));
$result['items'][] = $build;
// Re-run build validator:
$running = $this->validateRunningBuilds();
continue;
}
$builds++;
try {
// Logging relevant to this build should be stored
// against the build itself.
$buildDbLog = new BuildDBLogHandler($build, Logger::INFO);
$this->logger->pushHandler($buildDbLog);
$builder = new Builder($build, $this->logger);
$builder->execute();
// After execution we no longer want to record the information
// back to this specific build so the handler should be removed.
$this->logger->popHandler($buildDbLog);
} catch (\Exception $ex) {
$build->setStatus(Build::STATUS_FAILED);
$build->setFinished(new \DateTime());
$build->setLog($build->getLog() . PHP_EOL . PHP_EOL . $ex->getMessage());
$store->save($build);
}
}
$this->logger->addInfo(Lang::get('finished_processing_builds'));
return $builds;
}
public function setMaxBuilds($numBuilds)
{
$this->maxBuilds = (int)$numBuilds;
}
public function setDaemon($fromDaemon)
{
$this->isFromDaemon = (bool)$fromDaemon;
}
protected function validateRunningBuilds()
{
/** @var \PHPCI\Store\BuildStore $store */
$store = Factory::getStore('Build');
$running = $store->getByStatus(1);
$rtn = array();
$timeout = Config::getInstance()->get('phpci.build.failed_after', 1800);
foreach ($running['items'] as $build) {
/** @var \PHPCI\Model\Build $build */
$build = BuildFactory::getBuild($build);
$now = time();
$start = $build->getStarted()->getTimestamp();
if (($now - $start) > $timeout) {
$this->logger->addInfo(Lang::get('marked_as_failed', $build->getId()));
$build->setStatus(Build::STATUS_FAILED);
$build->setFinished(new \DateTime());
$store->save($build);
$build->removeBuildDirectory();
continue;
}
$rtn[$build->getProjectId()] = true;
}
return $rtn;
}
}

View file

@ -0,0 +1,68 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use b8\Config;
use Monolog\Logger;
use PHPCI\Helper\Lang;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Generate console command - Reads the database and generates models and stores.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Console
*/
class UpdateCommand extends Command
{
/**
* @var \Monolog\Logger
*/
protected $logger;
public function __construct(Logger $logger, $name = null)
{
parent::__construct($name);
$this->logger = $logger;
}
protected function configure()
{
$this
->setName('phpci:update')
->setDescription(Lang::get('update_phpci'));
}
/**
* Generates Model and Store classes by reading database meta data.
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
if (!$this->verifyInstalled($output)) {
return;
}
$output->write(Lang::get('updating_phpci'));
shell_exec(PHPCI_DIR . 'vendor/bin/phinx migrate -c "' . PHPCI_DIR . 'phinx.php"');
$output->writeln('<info>'.Lang::get('ok').'</info>');
}
protected function verifyInstalled(OutputInterface $output)
{
$config = Config::getInstance();
$phpciUrl = $config->get('phpci.url');
return !empty($phpciUrl);
}
}

View file

@ -0,0 +1,87 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2015, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use b8\Config;
use Monolog\Logger;
use PHPCI\Logging\OutputLogHandler;
use PHPCI\Worker\BuildWorker;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Worker Command - Starts the BuildWorker, which pulls jobs from beanstalkd
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Console
*/
class WorkerCommand extends Command
{
/**
* @var OutputInterface
*/
protected $output;
/**
* @var Logger
*/
protected $logger;
/**
* @param \Monolog\Logger $logger
* @param string $name
*/
public function __construct(Logger $logger, $name = null)
{
parent::__construct($name);
$this->logger = $logger;
}
protected function configure()
{
$this
->setName('phpci:worker')
->setDescription('Runs the PHPCI build worker.')
->addOption('debug', null, null, 'Run PHPCI in Debug Mode');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->output = $output;
// For verbose mode we want to output all informational and above
// messages to the symphony output interface.
if ($input->hasOption('verbose') && $input->getOption('verbose')) {
$this->logger->pushHandler(
new OutputLogHandler($this->output, Logger::INFO)
);
}
// Allow PHPCI to run in "debug mode"
if ($input->hasOption('debug') && $input->getOption('debug')) {
$output->writeln('<comment>Debug mode enabled.</comment>');
define('PHPCI_DEBUG_MODE', true);
}
$config = Config::getInstance()->get('phpci.worker', []);
if (empty($config['host']) || empty($config['queue'])) {
$error = 'The worker is not configured. You must set a host and queue in your config.yml file.';
throw new \Exception($error);
}
$worker = new BuildWorker($config['host'], $config['queue']);
$worker->setLogger($this->logger);
$worker->setMaxJobs(Config::getInstance()->get('phpci.worker.max_jobs', -1));
$worker->startWorker();
}
}

129
src/PHPCI/Controller.php Normal file
View file

@ -0,0 +1,129 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI;
use b8\Config;
use b8\Exception\HttpException\ForbiddenException;
use b8\Http\Request;
use b8\Http\Response;
use b8\View;
/**
* PHPCI Base Controller
* @package PHPCI
*/
class Controller extends \b8\Controller
{
/**
* @var \b8\View
*/
protected $controllerView;
/**
* @var \b8\View
*/
protected $view;
/**
* @var \b8\View
*/
public $layout;
/**
* Initialise the controller.
*/
public function init()
{
// Extended by actual controllers.
}
/**
* @param Config $config
* @param Request $request
* @param Response $response
*/
public function __construct(Config $config, Request $request, Response $response)
{
parent::__construct($config, $request, $response);
$class = explode('\\', get_class($this));
$this->className = substr(array_pop($class), 0, -10);
$this->setControllerView();
}
/**
* Set the view that this controller should use.
*/
protected function setControllerView()
{
if (View::exists($this->className)) {
$this->controllerView = new View($this->className);
} else {
$this->controllerView = new View\Template('{@content}');
}
}
/**
* Set the view that this controller action should use.
* @param $action
*/
protected function setView($action)
{
if (View::exists($this->className . '/' . $action)) {
$this->view = new View($this->className . '/' . $action);
}
}
/**
* Handle the incoming request.
* @param $action
* @param $actionParams
* @return \b8\b8\Http\Response|Response
*/
public function handleAction($action, $actionParams)
{
$this->setView($action);
$response = parent::handleAction($action, $actionParams);
if ($response instanceof Response) {
return $response;
}
if (is_string($response)) {
$this->controllerView->content = $response;
} elseif (isset($this->view)) {
$this->controllerView->content = $this->view->render();
}
$this->response->setContent($this->controllerView->render());
return $this->response;
}
/**
* Require that the currently logged in user is an administrator.
* @throws ForbiddenException
*/
protected function requireAdmin()
{
if (!$this->currentUserIsAdmin()) {
throw new ForbiddenException('You do not have permission to do that.');
}
}
/**
* Check if the currently logged in user is an administrator.
* @return bool
*/
protected function currentUserIsAdmin()
{
return $_SESSION['phpci_user']->getIsAdmin();
}
}

View file

@ -0,0 +1,279 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Controller;
use b8;
use b8\Exception\HttpException\NotFoundException;
use b8\Http\Response\JsonResponse;
use PHPCI\BuildFactory;
use PHPCI\Helper\AnsiConverter;
use PHPCI\Helper\Lang;
use PHPCI\Model\Build;
use PHPCI\Model\Project;
use PHPCI\Service\BuildService;
/**
* Build Controller - Allows users to run and view builds.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Web
*/
class BuildController extends \PHPCI\Controller
{
/**
* @var \PHPCI\Store\BuildStore
*/
protected $buildStore;
/**
* @var \PHPCI\Service\BuildService
*/
protected $buildService;
/**
* Initialise the controller, set up stores and services.
*/
public function init()
{
$this->buildStore = b8\Store\Factory::getStore('Build');
$this->buildService = new BuildService($this->buildStore);
}
/**
* View a specific build.
*/
public function view($buildId)
{
try {
$build = BuildFactory::getBuildById($buildId);
} catch (\Exception $ex) {
$build = null;
}
if (empty($build)) {
throw new NotFoundException(Lang::get('build_x_not_found', $buildId));
}
$this->view->plugins = $this->getUiPlugins();
$this->view->build = $build;
$this->view->data = $this->getBuildData($build);
$this->layout->title = Lang::get('build_n', $buildId);
$this->layout->subtitle = $build->getProjectTitle();
switch ($build->getStatus()) {
case 0:
$this->layout->skin = 'blue';
break;
case 1:
$this->layout->skin = 'yellow';
break;
case 2:
$this->layout->skin = 'green';
break;
case 3:
$this->layout->skin = 'red';
break;
}
$rebuild = Lang::get('rebuild_now');
$rebuildLink = PHPCI_URL . 'build/rebuild/' . $build->getId();
$delete = Lang::get('delete_build');
$deleteLink = PHPCI_URL . 'build/delete/' . $build->getId();
$actions = "<a class=\"btn btn-default\" href=\"{$rebuildLink}\">{$rebuild}</a> ";
if ($this->currentUserIsAdmin()) {
$actions .= " <a class=\"btn btn-danger\" href=\"{$deleteLink}\">{$delete}</a>";
}
$this->layout->actions = $actions;
}
/**
* Returns an array of the JS plugins to include.
* @return array
*/
protected function getUiPlugins()
{
$rtn = array();
$path = APPLICATION_PATH . 'public/assets/js/build-plugins/';
$dir = opendir($path);
while ($item = readdir($dir)) {
if (substr($item, 0, 1) == '.' || substr($item, -3) != '.js') {
continue;
}
$rtn[] = $item;
}
return $rtn;
}
/**
* AJAX call to get build data:
*/
public function data($buildId)
{
$response = new JsonResponse();
$build = BuildFactory::getBuildById($buildId);
if (!$build) {
$response->setResponseCode(404);
$response->setContent(array());
return $response;
}
$response->setContent($this->getBuildData($build));
return $response;
}
/**
* AJAX call to get build meta:
*/
public function meta($buildId)
{
$build = BuildFactory::getBuildById($buildId);
$key = $this->getParam('key', null);
$numBuilds = $this->getParam('num_builds', 1);
$data = null;
if ($key && $build) {
$data = $this->buildStore->getMeta($key, $build->getProjectId(), $buildId, $build->getBranch(), $numBuilds);
}
$response = new JsonResponse();
$response->setContent($data);
return $response;
}
/**
* Get build data from database and json encode it:
*/
protected function getBuildData(Build $build)
{
$data = array();
$data['status'] = (int)$build->getStatus();
$data['log'] = $this->cleanLog($build->getLog());
$data['created'] = !is_null($build->getCreated()) ? $build->getCreated()->format('Y-m-d H:i:s') : null;
$data['started'] = !is_null($build->getStarted()) ? $build->getStarted()->format('Y-m-d H:i:s') : null;
$data['finished'] = !is_null($build->getFinished()) ? $build->getFinished()->format('Y-m-d H:i:s') : null;
$data['duration'] = $build->getDuration();
/** @var \PHPCI\Store\BuildErrorStore $errorStore */
$errorStore = b8\Store\Factory::getStore('BuildError');
$errors = $errorStore->getErrorsForBuild($build->getId(), $this->getParam('since', null));
$errorView = new b8\View('Build/errors');
$errorView->build = $build;
$errorView->errors = $errors;
$data['errors'] = $errorStore->getErrorTotalForBuild($build->getId());
$data['error_html'] = $errorView->render();
$data['since'] = (new \DateTime())->format('Y-m-d H:i:s');
return $data;
}
/**
* Create a build using an existing build as a template:
*/
public function rebuild($buildId)
{
$copy = BuildFactory::getBuildById($buildId);
if (empty($copy)) {
throw new NotFoundException(Lang::get('build_x_not_found', $buildId));
}
$build = $this->buildService->createDuplicateBuild($copy);
if ($this->buildService->queueError) {
$_SESSION['global_error'] = Lang::get('add_to_queue_failed');
}
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL.'build/view/' . $build->getId());
return $response;
}
/**
* Delete a build.
*/
public function delete($buildId)
{
$this->requireAdmin();
$build = BuildFactory::getBuildById($buildId);
if (empty($build)) {
throw new NotFoundException(Lang::get('build_x_not_found', $buildId));
}
$this->buildService->deleteBuild($build);
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL.'project/view/' . $build->getProjectId());
return $response;
}
/**
* Parse log for unix colours and replace with HTML.
*/
protected function cleanLog($log)
{
return AnsiConverter::convert($log);
}
/**
* Allows the UI to poll for the latest running and pending builds.
*/
public function latest()
{
$rtn = array(
'pending' => $this->formatBuilds($this->buildStore->getByStatus(Build::STATUS_NEW)),
'running' => $this->formatBuilds($this->buildStore->getByStatus(Build::STATUS_RUNNING)),
);
$response = new JsonResponse();
$response->setContent($rtn);
return $response;
}
/**
* Formats a list of builds into rows suitable for the dropdowns in the PHPCI header bar.
* @param $builds
* @return array
*/
protected function formatBuilds($builds)
{
Project::$sleepable = array('id', 'title', 'reference', 'type');
$rtn = array('count' => $builds['count'], 'items' => array());
foreach ($builds['items'] as $build) {
$item = $build->toArray(1);
$header = new b8\View('Build/header-row');
$header->build = $build;
$item['header_row'] = $header->render();
$rtn['items'][$item['id']] = $item;
}
ksort($rtn['items']);
return $rtn;
}
}

View file

@ -0,0 +1,204 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Controller;
use b8;
use b8\Exception\HttpException\NotFoundException;
use b8\Store;
use PHPCI\BuildFactory;
use PHPCI\Model\Project;
use PHPCI\Model\Build;
use PHPCI\Service\BuildStatusService;
/**
* Build Status Controller - Allows external access to build status information / images.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Web
*/
class BuildStatusController extends \PHPCI\Controller
{
/* @var \PHPCI\Store\ProjectStore */
protected $projectStore;
/* @var \PHPCI\Store\BuildStore */
protected $buildStore;
/**
* Initialise the controller, set up stores and services.
*/
public function init()
{
$this->response->disableLayout();
$this->buildStore = Store\Factory::getStore('Build');
$this->projectStore = Store\Factory::getStore('Project');
}
/**
* Returns status of the last build
* @param $projectId
* @return string
*/
protected function getStatus($projectId)
{
$branch = $this->getParam('branch', 'master');
try {
$project = $this->projectStore->getById($projectId);
$status = 'passing';
if (!$project->getAllowPublicStatus()) {
return null;
}
if (isset($project) && $project instanceof Project) {
$build = $project->getLatestBuild($branch, array(2,3));
if (isset($build) && $build instanceof Build && $build->getStatus() != 2) {
$status = 'failed';
}
}
} catch (\Exception $e) {
$status = 'error';
}
return $status;
}
/**
* Displays projects information in ccmenu format
*
* @param $projectId
* @return bool
* @throws \Exception
* @throws b8\Exception\HttpException
*/
public function ccxml($projectId)
{
/* @var Project $project */
$project = $this->projectStore->getById($projectId);
$xml = new \SimpleXMLElement('<Projects/>');
if (!$project instanceof Project || !$project->getAllowPublicStatus()) {
return $this->renderXml($xml);
}
try {
$branchList = $this->buildStore->getBuildBranches($projectId);
if (!$branchList) {
$branchList = array($project->getBranch());
}
foreach ($branchList as $branch) {
$buildStatusService = new BuildStatusService($branch, $project, $project->getLatestBuild($branch));
if ($attributes = $buildStatusService->toArray()) {
$projectXml = $xml->addChild('Project');
foreach ($attributes as $attributeKey => $attributeValue) {
$projectXml->addAttribute($attributeKey, $attributeValue);
}
}
}
} catch (\Exception $e) {
$xml = new \SimpleXMLElement('<projects/>');
}
return $this->renderXml($xml);
}
/**
* @param \SimpleXMLElement $xml
* @return bool
*/
protected function renderXml(\SimpleXMLElement $xml = null)
{
$this->response->setHeader('Content-Type', 'text/xml');
$this->response->setContent($xml->asXML());
$this->response->flush();
echo $xml->asXML();
return true;
}
/**
* Returns the appropriate build status image in SVG format for a given project.
*/
public function image($projectId)
{
$style = $this->getParam('style', 'plastic');
$label = $this->getParam('label', 'build');
$status = $this->getStatus($projectId);
if (is_null($status)) {
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', '/');
return $response;
}
$color = ($status == 'passing') ? 'green' : 'red';
$image = file_get_contents(sprintf(
'http://img.shields.io/badge/%s-%s-%s.svg?style=%s',
$label,
$status,
$color,
$style
));
$this->response->disableLayout();
$this->response->setHeader('Content-Type', 'image/svg+xml');
$this->response->setContent($image);
return $this->response;
}
/**
* View the public status page of a given project, if enabled.
* @param $projectId
* @return string
* @throws \b8\Exception\HttpException\NotFoundException
*/
public function view($projectId)
{
$project = $this->projectStore->getById($projectId);
if (empty($project)) {
throw new NotFoundException('Project with id: ' . $projectId . ' not found');
}
if (!$project->getAllowPublicStatus()) {
throw new NotFoundException('Project with id: ' . $projectId . ' not found');
}
$builds = $this->getLatestBuilds($projectId);
if (count($builds)) {
$this->view->latest = $builds[0];
}
$this->view->builds = $builds;
$this->view->project = $project;
return $this->view->render();
}
/**
* Render latest builds for project as HTML table.
*/
protected function getLatestBuilds($projectId)
{
$criteria = array('project_id' => $projectId);
$order = array('id' => 'DESC');
$builds = $this->buildStore->getWhere($criteria, 10, 0, array(), $order);
foreach ($builds['items'] as &$build) {
$build = BuildFactory::getBuild($build);
}
return $builds['items'];
}
}

View file

@ -0,0 +1,121 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2015, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Controller;
use b8;
use b8\Form;
use b8\Store;
use PHPCI\Controller;
use PHPCI\Model\ProjectGroup;
use PHPCI\Helper\Lang;
/**
* Project Controller - Allows users to create, edit and view projects.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Web
*/
class GroupController extends Controller
{
/**
* @var \PHPCI\Store\ProjectGroupStore
*/
protected $groupStore;
/**
* Set up this controller.
*/
public function init()
{
$this->groupStore = b8\Store\Factory::getStore('ProjectGroup');
}
/**
* List project groups.
*/
public function index()
{
$this->requireAdmin();
$groups = array();
$groupList = $this->groupStore->getWhere(array(), 100, 0, array(), array('title' => 'ASC'));
foreach ($groupList['items'] as $group) {
$thisGroup = array(
'title' => $group->getTitle(),
'id' => $group->getId(),
);
$projects = b8\Store\Factory::getStore('Project')->getByGroupId($group->getId());
$thisGroup['projects'] = $projects['items'];
$groups[] = $thisGroup;
}
$this->view->groups = $groups;
}
/**
* Add or edit a project group.
* @param null $groupId
* @return void|b8\Http\Response\RedirectResponse
*/
public function edit($groupId = null)
{
$this->requireAdmin();
if (!is_null($groupId)) {
$group = $this->groupStore->getById($groupId);
} else {
$group = new ProjectGroup();
}
if ($this->request->getMethod() == 'POST') {
$group->setTitle($this->getParam('title'));
$this->groupStore->save($group);
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL.'group');
return $response;
}
$form = new Form();
$form->setMethod('POST');
$form->setAction(PHPCI_URL . 'group/edit' . (!is_null($groupId) ? '/' . $groupId : ''));
$title = new Form\Element\Text('title');
$title->setContainerClass('form-group');
$title->setClass('form-control');
$title->setLabel(Lang::get('group_title'));
$title->setValue($group->getTitle());
$submit = new Form\Element\Submit();
$submit->setValue(Lang::get('group_save'));
$form->addField($title);
$form->addField($submit);
$this->view->form = $form;
}
/**
* Delete a project group.
* @param $groupId
* @return b8\Http\Response\RedirectResponse
*/
public function delete($groupId)
{
$this->requireAdmin();
$group = $this->groupStore->getById($groupId);
$this->groupStore->delete($group);
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL.'group');
return $response;
}
}

View file

@ -0,0 +1,166 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Controller;
use b8;
use PHPCI\BuildFactory;
use PHPCI\Helper\Lang;
use PHPCI\Model\Build;
/**
* Home Controller - Displays the PHPCI Dashboard.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Web
*/
class HomeController extends \PHPCI\Controller
{
/**
* @var \PHPCI\Store\BuildStore
*/
protected $buildStore;
/**
* @var \PHPCI\Store\ProjectStore
*/
protected $projectStore;
/**
* @var \PHPCI\Store\ProjectGroupStore
*/
protected $groupStore;
/**
* Initialise the controller, set up stores and services.
*/
public function init()
{
$this->buildStore = b8\Store\Factory::getStore('Build');
$this->projectStore = b8\Store\Factory::getStore('Project');
$this->groupStore = b8\Store\Factory::getStore('ProjectGroup');
}
/**
* Display PHPCI dashboard:
*/
public function index()
{
$this->layout->title = Lang::get('dashboard');
$builds = $this->buildStore->getLatestBuilds(null, 10);
foreach ($builds as &$build) {
$build = BuildFactory::getBuild($build);
}
$this->view->builds = $builds;
$this->view->groups = $this->getGroupInfo();
return $this->view->render();
}
/**
* AJAX get latest builds table (HTML)
*/
public function latest()
{
$this->response->disableLayout();
$this->response->setContent($this->getLatestBuildsHtml());
return $this->response;
}
/**
* Ajax request for the project overview section of the dashboard.
*/
public function summary()
{
$this->response->disableLayout();
$projects = $this->projectStore->getWhere(array(), 50, 0, array(), array('title' => 'ASC'));
$this->response->setContent($this->getSummaryHtml($projects));
return $this->response;
}
/**
* Generate the HTML for the project overview section of the dashboard.
* @param $projects
* @return string
*/
protected function getSummaryHtml($projects)
{
$summaryBuilds = array();
$successes = array();
$failures = array();
$counts = array();
foreach ($projects as $project) {
$summaryBuilds[$project->getId()] = $this->buildStore->getLatestBuilds($project->getId());
$count = $this->buildStore->getWhere(
array('project_id' => $project->getId()),
1,
0,
array(),
array('id' => 'DESC')
);
$counts[$project->getId()] = $count['count'];
$success = $this->buildStore->getLastBuildByStatus($project->getId(), Build::STATUS_SUCCESS);
$failure = $this->buildStore->getLastBuildByStatus($project->getId(), Build::STATUS_FAILED);
$successes[$project->getId()] = $success;
$failures[$project->getId()] = $failure;
}
$summaryView = new b8\View('SummaryTable');
$summaryView->projects = $projects;
$summaryView->builds = $summaryBuilds;
$summaryView->successful = $successes;
$summaryView->failed = $failures;
$summaryView->counts = $counts;
return $summaryView->render();
}
/**
* Get latest builds and render as a table.
*/
protected function getLatestBuildsHtml()
{
$builds = $this->buildStore->getWhere(array(), 5, 0, array(), array('id' => 'DESC'));
$view = new b8\View('BuildsTable');
foreach ($builds['items'] as &$build) {
$build = BuildFactory::getBuild($build);
}
$view->builds = $builds['items'];
return $view->render();
}
/**
* Get a summary of the project groups we have, and what projects they have in them.
* @return array
*/
protected function getGroupInfo()
{
$rtn = array();
$groups = $this->groupStore->getWhere(array(), 100, 0, array(), array('title' => 'ASC'));
foreach ($groups['items'] as $group) {
$thisGroup = array('title' => $group->getTitle());
$projects = $this->projectStore->getByGroupId($group->getId());
$thisGroup['projects'] = $projects['items'];
$thisGroup['summary'] = $this->getSummaryHtml($thisGroup['projects']);
$rtn[] = $thisGroup;
}
return $rtn;
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Controller;
use b8;
use PHPCI\Helper\Lang;
use PHPCI\Plugin\Util\ComposerPluginInformation;
use PHPCI\Plugin\Util\FilesPluginInformation;
use PHPCI\Plugin\Util\PluginInformationCollection;
/**
* Plugin Controller - Provides support for installing Composer packages.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Web
*/
class PluginController extends \PHPCI\Controller
{
/**
* List all enabled plugins, installed and recommend packages.
* @return string
*/
public function index()
{
$this->requireAdmin();
$json = $this->getComposerJson();
$this->view->installedPackages = $json['require'];
$pluginInfo = new PluginInformationCollection();
$pluginInfo->add(FilesPluginInformation::newFromDir(
PHPCI_DIR . "PHPCI/Plugin/"
));
$pluginInfo->add(ComposerPluginInformation::buildFromYaml(
PHPCI_DIR . "vendor/composer/installed.json"
));
$this->view->plugins = $pluginInfo->getInstalledPlugins();
$this->layout->title = Lang::get('plugins');
return $this->view->render();
}
/**
* Get the json-decoded contents of the composer.json file.
* @return mixed
*/
protected function getComposerJson()
{
$json = file_get_contents(APPLICATION_PATH . 'composer.json');
return json_decode($json, true);
}
}

View file

@ -0,0 +1,451 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Controller;
use b8;
use b8\Form;
use b8\Exception\HttpException\NotFoundException;
use b8\Store;
use PHPCI;
use PHPCI\BuildFactory;
use PHPCI\Helper\Github;
use PHPCI\Helper\Lang;
use PHPCI\Helper\SshKey;
use PHPCI\Service\BuildService;
use PHPCI\Service\ProjectService;
/**
* Project Controller - Allows users to create, edit and view projects.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Web
*/
class ProjectController extends PHPCI\Controller
{
/**
* @var \PHPCI\Store\ProjectStore
*/
protected $projectStore;
/**
* @var \PHPCI\Service\ProjectService
*/
protected $projectService;
/**
* @var \PHPCI\Store\BuildStore
*/
protected $buildStore;
/**
* @var \PHPCI\Service\BuildService
*/
protected $buildService;
/**
* Initialise the controller, set up stores and services.
*/
public function init()
{
$this->buildStore = Store\Factory::getStore('Build');
$this->projectStore = Store\Factory::getStore('Project');
$this->projectService = new ProjectService($this->projectStore);
$this->buildService = new BuildService($this->buildStore);
}
/**
* View a specific project.
*/
public function view($projectId)
{
$branch = $this->getParam('branch', '');
$project = $this->projectStore->getById($projectId);
if (empty($project)) {
throw new NotFoundException(Lang::get('project_x_not_found', $projectId));
}
$per_page = 10;
$page = $this->getParam('p', 1);
$builds = $this->getLatestBuildsHtml($projectId, urldecode($branch), (($page - 1) * $per_page));
$pages = $builds[1] == 0 ? 1 : ceil($builds[1] / $per_page);
if ($page > $pages) {
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL.'project/view/'.$projectId);
return $response;
}
$this->view->builds = $builds[0];
$this->view->total = $builds[1];
$this->view->project = $project;
$this->view->branch = urldecode($branch);
$this->view->branches = $this->projectStore->getKnownBranches($projectId);
$this->view->page = $page;
$this->view->pages = $pages;
$this->layout->title = $project->getTitle();
$this->layout->subtitle = $this->view->branch;
return $this->view->render();
}
/**
* Create a new pending build for a project.
*/
public function build($projectId, $branch = '')
{
/* @var \PHPCI\Model\Project $project */
$project = $this->projectStore->getById($projectId);
if (empty($branch)) {
$branch = $project->getBranch();
}
if (empty($project)) {
throw new NotFoundException(Lang::get('project_x_not_found', $projectId));
}
$email = $_SESSION['phpci_user']->getEmail();
$build = $this->buildService->createBuild($project, null, urldecode($branch), $email);
if ($this->buildService->queueError) {
$_SESSION['global_error'] = Lang::get('add_to_queue_failed');
}
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL.'build/view/' . $build->getId());
return $response;
}
/**
* Delete a project.
*/
public function delete($projectId)
{
$this->requireAdmin();
$project = $this->projectStore->getById($projectId);
$this->projectService->deleteProject($project);
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL);
return $response;
}
/**
* AJAX get latest builds.
*/
public function builds($projectId)
{
$branch = $this->getParam('branch', '');
$builds = $this->getLatestBuildsHtml($projectId, urldecode($branch));
$this->response->disableLayout();
$this->response->setContent($builds[0]);
return $this->response;
}
/**
* Render latest builds for project as HTML table.
*
* @param $projectId
* @param string $branch A urldecoded branch name.
* @param int $start
* @return array
*/
protected function getLatestBuildsHtml($projectId, $branch = '', $start = 0)
{
$criteria = array('project_id' => $projectId);
if (!empty($branch)) {
$criteria['branch'] = $branch;
}
$order = array('id' => 'DESC');
$builds = $this->buildStore->getWhere($criteria, 10, $start, array(), $order);
$view = new b8\View('BuildsTable');
foreach ($builds['items'] as &$build) {
$build = BuildFactory::getBuild($build);
}
$view->builds = $builds['items'];
return array($view->render(), $builds['count']);
}
/**
* Add a new project. Handles both the form, and processing.
*/
public function add()
{
$this->layout->title = Lang::get('add_project');
$this->requireAdmin();
$method = $this->request->getMethod();
$pub = null;
$values = $this->getParams();
if ($method != 'POST') {
$sshKey = new SshKey();
$key = $sshKey->generate();
$values['key'] = $key['private_key'];
$values['pubkey'] = $key['public_key'];
$pub = $key['public_key'];
}
$form = $this->projectForm($values);
if ($method != 'POST' || ($method == 'POST' && !$form->validate())) {
$view = new b8\View('ProjectForm');
$view->type = 'add';
$view->project = null;
$view->form = $form;
$view->key = $pub;
return $view->render();
} else {
$title = $this->getParam('title', 'New Project');
$reference = $this->getParam('reference', null);
$type = $this->getParam('type', null);
$options = array(
'ssh_private_key' => $this->getParam('key', null),
'ssh_public_key' => $this->getParam('pubkey', null),
'build_config' => $this->getParam('build_config', null),
'allow_public_status' => $this->getParam('allow_public_status', 0),
'branch' => $this->getParam('branch', null),
'group' => $this->getParam('group_id', null),
);
$project = $this->projectService->createProject($title, $type, $reference, $options);
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL.'project/view/' . $project->getId());
return $response;
}
}
/**
* Edit a project. Handles both the form and processing.
*/
public function edit($projectId)
{
$this->requireAdmin();
$method = $this->request->getMethod();
$project = $this->projectStore->getById($projectId);
if (empty($project)) {
throw new NotFoundException(Lang::get('project_x_not_found', $projectId));
}
$this->layout->title = $project->getTitle();
$this->layout->subtitle = Lang::get('edit_project');
$values = $project->getDataArray();
$values['key'] = $values['ssh_private_key'];
$values['pubkey'] = $values['ssh_public_key'];
if ($values['type'] == "gitlab") {
$accessInfo = $project->getAccessInformation();
$reference = $accessInfo["user"].'@'.$accessInfo["domain"].':' . $project->getReference().".git";
$values['reference'] = $reference;
}
if ($method == 'POST') {
$values = $this->getParams();
}
$form = $this->projectForm($values, 'edit/' . $projectId);
if ($method != 'POST' || ($method == 'POST' && !$form->validate())) {
$view = new b8\View('ProjectForm');
$view->type = 'edit';
$view->project = $project;
$view->form = $form;
$view->key = $values['pubkey'];
return $view->render();
}
$title = $this->getParam('title', Lang::get('new_project'));
$reference = $this->getParam('reference', null);
$type = $this->getParam('type', null);
$options = array(
'ssh_private_key' => $this->getParam('key', null),
'ssh_public_key' => $this->getParam('pubkey', null),
'build_config' => $this->getParam('build_config', null),
'allow_public_status' => $this->getParam('allow_public_status', 0),
'archived' => $this->getParam('archived', 0),
'branch' => $this->getParam('branch', null),
'group' => $this->getParam('group_id', null),
);
$project = $this->projectService->updateProject($project, $title, $type, $reference, $options);
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL.'project/view/' . $project->getId());
return $response;
}
/**
* Create add / edit project form.
*/
protected function projectForm($values, $type = 'add')
{
$form = new Form();
$form->setMethod('POST');
$form->setAction(PHPCI_URL.'project/' . $type);
$form->addField(new Form\Element\Csrf('csrf'));
$form->addField(new Form\Element\Hidden('pubkey'));
$options = array(
'choose' => Lang::get('select_repository_type'),
'github' => Lang::get('github'),
'bitbucket' => Lang::get('bitbucket'),
'gitlab' => Lang::get('gitlab'),
'remote' => Lang::get('remote'),
'local' => Lang::get('local'),
'hg' => Lang::get('hg'),
'svn' => Lang::get('svn'),
);
$field = Form\Element\Select::create('type', Lang::get('where_hosted'), true);
$field->setPattern('^(github|bitbucket|gitlab|remote|local|hg|svn)');
$field->setOptions($options);
$field->setClass('form-control')->setContainerClass('form-group');
$form->addField($field);
$container = new Form\ControlGroup('github-container');
$container->setClass('github-container');
$field = Form\Element\Select::create('github', Lang::get('choose_github'), false);
$field->setClass('form-control')->setContainerClass('form-group');
$container->addField($field);
$form->addField($container);
$field = Form\Element\Text::create('reference', Lang::get('repo_name'), true);
$field->setValidator($this->getReferenceValidator($values));
$field->setClass('form-control')->setContainerClass('form-group');
$form->addField($field);
$field = Form\Element\Text::create('title', Lang::get('project_title'), true);
$field->setClass('form-control')->setContainerClass('form-group');
$form->addField($field);
$field = Form\Element\TextArea::create('key', Lang::get('project_private_key'), false);
$field->setClass('form-control')->setContainerClass('form-group');
$field->setRows(6);
$form->addField($field);
$field = Form\Element\TextArea::create('build_config', Lang::get('build_config'), false);
$field->setClass('form-control')->setContainerClass('form-group');
$field->setRows(6);
$form->addField($field);
$field = Form\Element\Text::create('branch', Lang::get('default_branch'), true);
$field->setClass('form-control')->setContainerClass('form-group')->setValue('master');
$form->addField($field);
$field = Form\Element\Select::create('group_id', 'Project Group', true);
$field->setClass('form-control')->setContainerClass('form-group')->setValue(1);
$groups = array();
$groupStore = b8\Store\Factory::getStore('ProjectGroup');
$groupList = $groupStore->getWhere(array(), 100, 0, array(), array('title' => 'ASC'));
foreach ($groupList['items'] as $group) {
$groups[$group->getId()] = $group->getTitle();
}
$field->setOptions($groups);
$form->addField($field);
$field = Form\Element\Checkbox::create('allow_public_status', Lang::get('allow_public_status'), false);
$field->setContainerClass('form-group');
$field->setCheckedValue(1);
$field->setValue(0);
$form->addField($field);
$field = Form\Element\Checkbox::create('archived', Lang::get('archived'), false);
$field->setContainerClass('form-group');
$field->setCheckedValue(1);
$field->setValue(0);
$form->addField($field);
$field = new Form\Element\Submit();
$field->setValue(Lang::get('save_project'));
$field->setContainerClass('form-group');
$field->setClass('btn-success');
$form->addField($field);
$form->setValues($values);
return $form;
}
/**
* Get an array of repositories from Github's API.
*/
protected function githubRepositories()
{
$github = new Github();
$response = new b8\Http\Response\JsonResponse();
$response->setContent($github->getRepositories());
return $response;
}
/**
* Get the validator to use to check project references.
* @param $values
* @return callable
*/
protected function getReferenceValidator($values)
{
return function ($val) use ($values) {
$type = $values['type'];
$validators = array(
'hg' => array(
'regex' => '/^(https?):\/\//',
'message' => Lang::get('error_mercurial')
),
'remote' => array(
'regex' => '/^(git|https?):\/\//',
'message' => Lang::get('error_remote')
),
'gitlab' => array(
'regex' => '`^(.*)@(.*):(.*)/(.*)\.git`',
'message' => Lang::get('error_gitlab')
),
'github' => array(
'regex' => '/^[a-zA-Z0-9_\-]+\/[a-zA-Z0-9_\-\.]+$/',
'message' => Lang::get('error_github')
),
'bitbucket' => array(
'regex' => '/^[a-zA-Z0-9_\-]+\/[a-zA-Z0-9_\-\.]+$/',
'message' => Lang::get('error_bitbucket')
),
);
if (in_array($type, $validators) && !preg_match($validators[$type]['regex'], $val)) {
throw new \Exception($validators[$type]['message']);
} elseif ($type == 'local' && !is_dir($val)) {
throw new \Exception(Lang::get('error_path'));
}
return true;
};
}
}

View file

@ -0,0 +1,213 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Controller;
use b8;
use PHPCI\Helper\Email;
use PHPCI\Helper\Lang;
/**
* Session Controller - Handles user login / logout.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Web
*/
class SessionController extends \PHPCI\Controller
{
/**
* @var \PHPCI\Store\UserStore
*/
protected $userStore;
/**
* Initialise the controller, set up stores and services.
*/
public function init()
{
$this->response->disableLayout();
$this->userStore = b8\Store\Factory::getStore('User');
}
/**
* Handles user login (form and processing)
*/
public function login()
{
$isLoginFailure = false;
if ($this->request->getMethod() == 'POST') {
$token = $this->getParam('token');
if (!isset($token, $_SESSION['login_token']) || $token !== $_SESSION['login_token']) {
$isLoginFailure = true;
} else {
unset($_SESSION['login_token']);
$user = $this->userStore->getByEmail($this->getParam('email'));
if ($user && password_verify($this->getParam('password', ''), $user->getHash())) {
session_regenerate_id(true);
$_SESSION['phpci_user_id'] = $user->getId();
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', $this->getLoginRedirect());
return $response;
} else {
$isLoginFailure = true;
}
}
}
$form = new b8\Form();
$form->setMethod('POST');
$form->setAction(PHPCI_URL.'session/login');
$email = new b8\Form\Element\Email('email');
$email->setLabel(Lang::get('email_address'));
$email->setRequired(true);
$email->setContainerClass('form-group');
$email->setClass('form-control');
$form->addField($email);
$pwd = new b8\Form\Element\Password('password');
$pwd->setLabel(Lang::get('password'));
$pwd->setRequired(true);
$pwd->setContainerClass('form-group');
$pwd->setClass('form-control');
$form->addField($pwd);
$pwd = new b8\Form\Element\Submit();
$pwd->setValue(Lang::get('log_in'));
$pwd->setClass('btn-success');
$form->addField($pwd);
$tokenValue = $this->generateToken();
$_SESSION['login_token'] = $tokenValue;
$token = new b8\Form\Element\Hidden('token');
$token->setValue($tokenValue);
$form->addField($token);
$this->view->form = $form->render();
$this->view->failed = $isLoginFailure;
return $this->view->render();
}
/**
* Handles user logout.
*/
public function logout()
{
unset($_SESSION['phpci_user']);
unset($_SESSION['phpci_user_id']);
session_destroy();
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL);
return $response;
}
/**
* Allows the user to request a password reset email.
* @return string
*/
public function forgotPassword()
{
if ($this->request->getMethod() == 'POST') {
$email = $this->getParam('email', null);
$user = $this->userStore->getByEmail($email);
if (empty($user)) {
$this->view->error = Lang::get('reset_no_user_exists');
return $this->view->render();
}
$key = md5(date('Y-m-d') . $user->getHash());
$url = PHPCI_URL;
$message = Lang::get('reset_email_body', $user->getName(), $url, $user->getId(), $key);
$email = new Email();
$email->setEmailTo($user->getEmail(), $user->getName());
$email->setSubject(Lang::get('reset_email_title', $user->getName()));
$email->setBody($message);
$email->send();
$this->view->emailed = true;
}
return $this->view->render();
}
/**
* Allows the user to change their password after a password reset email.
* @param $userId
* @param $key
* @return string
*/
public function resetPassword($userId, $key)
{
$user = $this->userStore->getById($userId);
$userKey = md5(date('Y-m-d') . $user->getHash());
if (empty($user) || $key != $userKey) {
$this->view->error = Lang::get('reset_invalid');
return $this->view->render();
}
if ($this->request->getMethod() == 'POST') {
$hash = password_hash($this->getParam('password'), PASSWORD_DEFAULT);
$user->setHash($hash);
$_SESSION['phpci_user'] = $this->userStore->save($user);
$_SESSION['phpci_user_id'] = $user->getId();
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL);
return $response;
}
$this->view->id = $userId;
$this->view->key = $key;
return $this->view->render();
}
/**
* Get the URL the user was trying to go to prior to being asked to log in.
* @return string
*/
protected function getLoginRedirect()
{
$rtn = PHPCI_URL;
if (!empty($_SESSION['phpci_login_redirect'])) {
$rtn .= $_SESSION['phpci_login_redirect'];
$_SESSION['phpci_login_redirect'] = null;
}
return $rtn;
}
/** Generate a random token.
*
* @return string
*/
protected function generateToken()
{
if (function_exists('openssl_random_pseudo_bytes')) {
return bin2hex(openssl_random_pseudo_bytes(16));
}
return sprintf("%04x", mt_rand(0, 0xFFFF))
. sprintf("%04x", mt_rand(0, 0xFFFF))
. sprintf("%04x", mt_rand(0, 0xFFFF))
. sprintf("%04x", mt_rand(0, 0xFFFF));
}
}

View file

@ -0,0 +1,496 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Controller;
use b8;
use b8\Form;
use b8\HttpClient;
use PHPCI\Controller;
use PHPCI\Helper\Lang;
use Symfony\Component\Yaml\Dumper;
use Symfony\Component\Yaml\Parser;
/**
* Settings Controller
*
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Web
*/
class SettingsController extends Controller
{
/**
* @var array
*/
protected $settings;
/**
* Initialise the controller, set up stores and services.
*/
public function init()
{
parent::init();
$parser = new Parser();
$yaml = file_get_contents(PHPCI_CONFIG_FILE);
$this->settings = $parser->parse($yaml);
}
/**
* Display settings forms.
* @return string
*/
public function index()
{
$this->requireAdmin();
$this->layout->title = Lang::get('settings');
$this->view->settings = $this->settings;
$basicSettings = array();
if (isset($this->settings['phpci']['basic'])) {
$basicSettings = $this->settings['phpci']['basic'];
}
$buildSettings = array();
if (isset($this->settings['phpci']['build'])) {
$buildSettings = $this->settings['phpci']['build'];
}
$emailSettings = array();
if (isset($this->settings['phpci']['email_settings'])) {
$emailSettings = $this->settings['phpci']['email_settings'];
}
$authSettings = array();
if (isset($this->settings['phpci']['authentication_settings'])) {
$authSettings = $this->settings['phpci']['authentication_settings'];
}
$this->view->configFile = PHPCI_CONFIG_FILE;
$this->view->basicSettings = $this->getBasicForm($basicSettings);
$this->view->buildSettings = $this->getBuildForm($buildSettings);
$this->view->github = $this->getGithubForm();
$this->view->emailSettings = $this->getEmailForm($emailSettings);
$this->view->authenticationSettings = $this->getAuthenticationForm($authSettings);
$this->view->isWriteable = $this->canWriteConfig();
if (!empty($this->settings['phpci']['github']['token'])) {
$this->view->githubUser = $this->getGithubUser($this->settings['phpci']['github']['token']);
}
return $this->view->render();
}
/**
* Save Github settings.
*/
public function github()
{
$this->requireAdmin();
$this->settings['phpci']['github']['id'] = $this->getParam('githubid', '');
$this->settings['phpci']['github']['secret'] = $this->getParam('githubsecret', '');
$error = $this->storeSettings();
$response = new b8\Http\Response\RedirectResponse();
if ($error) {
$response->setHeader('Location', PHPCI_URL . 'settings?saved=2');
} else {
$response->setHeader('Location', PHPCI_URL . 'settings?saved=1');
}
return $response;
}
/**
* Save email settings.
*/
public function email()
{
$this->requireAdmin();
$this->settings['phpci']['email_settings'] = $this->getParams();
$this->settings['phpci']['email_settings']['smtp_encryption'] = $this->getParam('smtp_encryption', 0);
$error = $this->storeSettings();
$response = new b8\Http\Response\RedirectResponse();
if ($error) {
$response->setHeader('Location', PHPCI_URL . 'settings?saved=2');
} else {
$response->setHeader('Location', PHPCI_URL . 'settings?saved=1');
}
return $response;
}
/**
* Save build settings.
*/
public function build()
{
$this->requireAdmin();
$this->settings['phpci']['build'] = $this->getParams();
$error = $this->storeSettings();
$response = new b8\Http\Response\RedirectResponse();
if ($error) {
$response->setHeader('Location', PHPCI_URL . 'settings?saved=2');
} else {
$response->setHeader('Location', PHPCI_URL . 'settings?saved=1');
}
return $response;
}
/**
* Save basic settings.
*/
public function basic()
{
$this->requireAdmin();
$this->settings['phpci']['basic'] = $this->getParams();
$error = $this->storeSettings();
$response = new b8\Http\Response\RedirectResponse();
if ($error) {
$response->setHeader('Location', PHPCI_URL . 'settings?saved=2');
} else {
$response->setHeader('Location', PHPCI_URL . 'settings?saved=1');
}
return $response;
}
/**
* Handle authentication settings
*/
public function authentication()
{
$this->requireAdmin();
$this->settings['phpci']['authentication_settings']['state'] = $this->getParam('disable_authentication', 0);
$this->settings['phpci']['authentication_settings']['user_id'] = $_SESSION['phpci_user_id'];
$error = $this->storeSettings();
$response = new b8\Http\Response\RedirectResponse();
if ($error) {
$response->setHeader('Location', PHPCI_URL . 'settings?saved=2');
} else {
$response->setHeader('Location', PHPCI_URL . 'settings?saved=1');
}
return $response;
}
/**
* Github redirects users back to this URL when t
*/
public function githubCallback()
{
$code = $this->getParam('code', null);
$github = $this->settings['phpci']['github'];
if (!is_null($code)) {
$http = new HttpClient();
$url = 'https://github.com/login/oauth/access_token';
$params = array('client_id' => $github['id'], 'client_secret' => $github['secret'], 'code' => $code);
$resp = $http->post($url, $params);
if ($resp['success']) {
parse_str($resp['body'], $resp);
$this->settings['phpci']['github']['token'] = $resp['access_token'];
$this->storeSettings();
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL . 'settings?linked=1');
return $response;
}
}
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL . 'settings?linked=2');
return $response;
}
/**
* Convert config to yaml and store to file.
*
* @return mixed
*/
protected function storeSettings()
{
$dumper = new Dumper();
$yaml = $dumper->dump($this->settings, 4);
file_put_contents(PHPCI_CONFIG_FILE, $yaml);
if (error_get_last()) {
$error_get_last = error_get_last();
return $error_get_last['message'];
}
}
/**
* Get the Github settings form.
* @return Form
*/
protected function getGithubForm()
{
$form = new Form();
$form->setMethod('POST');
$form->setAction(PHPCI_URL . 'settings/github');
$form->addField(new Form\Element\Csrf('csrf'));
$field = new Form\Element\Text('githubid');
$field->setRequired(true);
$field->setPattern('[a-zA-Z0-9]+');
$field->setLabel(Lang::get('application_id'));
$field->setClass('form-control');
$field->setContainerClass('form-group');
$form->addField($field);
if (isset($this->settings['phpci']['github']['id'])) {
$field->setValue($this->settings['phpci']['github']['id']);
}
$field = new Form\Element\Text('githubsecret');
$field->setRequired(true);
$field->setPattern('[a-zA-Z0-9]+');
$field->setLabel(Lang::get('application_secret'));
$field->setClass('form-control');
$field->setContainerClass('form-group');
$form->addField($field);
if (isset($this->settings['phpci']['github']['secret'])) {
$field->setValue($this->settings['phpci']['github']['secret']);
}
$field = new Form\Element\Submit();
$field->setValue(Lang::get('save'));
$field->setClass('btn btn-success pull-right');
$form->addField($field);
return $form;
}
/**
* Get the email settings form.
* @param array $values
* @return Form
*/
protected function getEmailForm($values = array())
{
$form = new Form();
$form->setMethod('POST');
$form->setAction(PHPCI_URL . 'settings/email');
$form->addField(new Form\Element\Csrf('csrf'));
$field = new Form\Element\Text('smtp_address');
$field->setRequired(false);
$field->setLabel(Lang::get('smtp_server'));
$field->setClass('form-control');
$field->setContainerClass('form-group');
$field->setValue('localhost');
$form->addField($field);
$field = new Form\Element\Text('smtp_port');
$field->setRequired(false);
$field->setPattern('[0-9]+');
$field->setLabel(Lang::get('smtp_port'));
$field->setClass('form-control');
$field->setContainerClass('form-group');
$field->setValue(25);
$form->addField($field);
$field = new Form\Element\Text('smtp_username');
$field->setRequired(false);
$field->setLabel(Lang::get('smtp_username'));
$field->setClass('form-control');
$field->setContainerClass('form-group');
$form->addField($field);
$field = new Form\Element\Password('smtp_password');
$field->setRequired(false);
$field->setLabel(Lang::get('smtp_password'));
$field->setClass('form-control');
$field->setContainerClass('form-group');
$form->addField($field);
$field = new Form\Element\Email('from_address');
$field->setRequired(false);
$field->setLabel(Lang::get('from_email_address'));
$field->setClass('form-control');
$field->setContainerClass('form-group');
$form->addField($field);
$field = new Form\Element\Email('default_mailto_address');
$field->setRequired(false);
$field->setLabel(Lang::get('default_notification_address'));
$field->setClass('form-control');
$field->setContainerClass('form-group');
$form->addField($field);
$field = new Form\Element\Select('smtp_encryption');
$field->setOptions(array('' => Lang::get('none'), 'tls' => Lang::get('tls'), 'ssl' => Lang::get('ssl')));
$field->setRequired(false);
$field->setLabel(Lang::get('use_smtp_encryption'));
$field->setContainerClass('form-group');
$field->setValue(1);
$form->addField($field);
$field = new Form\Element\Submit();
$field->setValue(Lang::get('save'));
$field->setClass('btn btn-success pull-right');
$form->addField($field);
$form->setValues($values);
return $form;
}
/**
* Call Github API for our Github user object.
* @param $token
* @return mixed
*/
protected function getGithubUser($token)
{
$http = new HttpClient('https://api.github.com');
$user = $http->get('/user', array('access_token' => $token));
return $user['body'];
}
/**
* Check if we can write the PHPCI config file.
* @return bool
*/
protected function canWriteConfig()
{
return is_writeable(PHPCI_CONFIG_FILE);
}
/**
* Get the Build settings form.
* @param array $values
* @return Form
*/
protected function getBuildForm($values = array())
{
$form = new Form();
$form->setMethod('POST');
$form->setAction(PHPCI_URL . 'settings/build');
$field = new Form\Element\Select('failed_after');
$field->setRequired(false);
$field->setLabel(Lang::get('failed_after'));
$field->setClass('form-control');
$field->setContainerClass('form-group');
$field->setOptions(array(
300 => Lang::get('5_mins'),
900 => Lang::get('15_mins'),
1800 => Lang::get('30_mins'),
3600 => Lang::get('1_hour'),
10800 => Lang::get('3_hours'),
));
$field->setValue(1800);
$form->addField($field);
$field = new Form\Element\Submit();
$field->setValue(Lang::get('save'));
$field->setClass('btn btn-success pull-right');
$form->addField($field);
$form->setValues($values);
return $form;
}
/**
* Get the Basic settings form.
* @param array $values
* @return Form
*/
protected function getBasicForm($values = array())
{
$form = new Form();
$form->setMethod('POST');
$form->setAction(PHPCI_URL . 'settings/basic');
$field = new Form\Element\Select('language');
$field->setRequired(true);
$field->setLabel(Lang::get('language'));
$field->setClass('form-control');
$field->setContainerClass('form-group');
$field->setOptions(Lang::getLanguageOptions());
$field->setValue(Lang::getLanguage());
$form->addField($field);
$field = new Form\Element\Submit();
$field->setValue(Lang::get('save'));
$field->setClass('btn btn-success pull-right');
$form->addField($field);
$form->setValues($values);
return $form;
}
/**
* Form for disabling user authentication while using a default user
*
* @param array $values
* @return Form
*/
protected function getAuthenticationForm($values = array())
{
$form = new Form();
$form->setMethod('POST');
$form->setAction(PHPCI_URL . 'settings/authentication');
$form->addField(new Form\Element\Csrf('csrf'));
$field = new Form\Element\Checkbox('disable_authentication');
$field->setCheckedValue(1);
$field->setRequired(false);
$field->setLabel('Disable Authentication?');
$field->setContainerClass('form-group');
$field->setValue(0);
if (isset($values['state'])) {
$field->setValue((int)$values['state']);
}
$form->addField($field);
$field = new Form\Element\Submit();
$field->setValue('Save &raquo;');
$field->setClass('btn btn-success pull-right');
$form->addField($field);
$form->setValues($values);
return $form;
}
}

View file

@ -0,0 +1,296 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Controller;
use b8;
use b8\Exception\HttpException\NotFoundException;
use b8\Form;
use PHPCI\Controller;
use PHPCI\Helper\Lang;
use PHPCI\Service\UserService;
/**
* User Controller - Allows an administrator to view, add, edit and delete users.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Web
*/
class UserController extends Controller
{
/**
* @var \PHPCI\Store\UserStore
*/
protected $userStore;
/**
* @var \PHPCI\Service\UserService
*/
protected $userService;
/**
* Initialise the controller, set up stores and services.
*/
public function init()
{
$this->userStore = b8\Store\Factory::getStore('User');
$this->userService = new UserService($this->userStore);
}
/**
* View user list.
*/
public function index()
{
$users = $this->userStore->getWhere(array(), 1000, 0, array(), array('email' => 'ASC'));
$this->view->users = $users;
$this->layout->title = Lang::get('manage_users');
return $this->view->render();
}
/**
* Allows the user to edit their profile.
* @return string
*/
public function profile()
{
$user = $_SESSION['phpci_user'];
if ($this->request->getMethod() == 'POST') {
$name = $this->getParam('name', null);
$email = $this->getParam('email', null);
$password = $this->getParam('password', null);
$currentLang = Lang::getLanguage();
$chosenLang = $this->getParam('language', $currentLang);
if ($chosenLang !== $currentLang) {
setcookie('phpcilang', $chosenLang, time() + (10 * 365 * 24 * 60 * 60), '/');
Lang::setLanguage($chosenLang);
}
$_SESSION['phpci_user'] = $this->userService->updateUser($user, $name, $email, $password);
$user = $_SESSION['phpci_user'];
$this->view->updated = 1;
}
$this->layout->title = $user->getName();
$this->layout->subtitle = Lang::get('edit_profile');
$values = $user->getDataArray();
if (array_key_exists('phpcilang', $_COOKIE)) {
$values['language'] = $_COOKIE['phpcilang'];
}
$form = new Form();
$form->setAction(PHPCI_URL.'user/profile');
$form->setMethod('POST');
$name = new Form\Element\Text('name');
$name->setClass('form-control');
$name->setContainerClass('form-group');
$name->setLabel(Lang::get('name'));
$name->setRequired(true);
$form->addField($name);
$email = new Form\Element\Email('email');
$email->setClass('form-control');
$email->setContainerClass('form-group');
$email->setLabel(Lang::get('email_address'));
$email->setRequired(true);
$form->addField($email);
$password = new Form\Element\Password('password');
$password->setClass('form-control');
$password->setContainerClass('form-group');
$password->setLabel(Lang::get('password_change'));
$password->setRequired(false);
$form->addField($password);
$lang = new Form\Element\Select('language');
$lang->setClass('form-control');
$lang->setContainerClass('form-group');
$lang->setLabel(Lang::get('language'));
$lang->setRequired(true);
$lang->setOptions(Lang::getLanguageOptions());
$lang->setValue(Lang::getLanguage());
$form->addField($lang);
$submit = new Form\Element\Submit();
$submit->setClass('btn btn-success');
$submit->setValue(Lang::get('save'));
$form->addField($submit);
$form->setValues($values);
$this->view->form = $form;
return $this->view->render();
}
/**
* Add a user - handles both form and processing.
*/
public function add()
{
$this->requireAdmin();
$this->layout->title = Lang::get('add_user');
$method = $this->request->getMethod();
if ($method == 'POST') {
$values = $this->getParams();
} else {
$values = array();
}
$form = $this->userForm($values);
if ($method != 'POST' || ($method == 'POST' && !$form->validate())) {
$view = new b8\View('UserForm');
$view->type = 'add';
$view->user = null;
$view->form = $form;
return $view->render();
}
$name = $this->getParam('name', null);
$email = $this->getParam('email', null);
$password = $this->getParam('password', null);
$isAdmin = (int)$this->getParam('is_admin', 0);
$this->userService->createUser($name, $email, $password, $isAdmin);
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL . 'user');
return $response;
}
/**
* Edit a user - handles both form and processing.
*/
public function edit($userId)
{
$this->requireAdmin();
$method = $this->request->getMethod();
$user = $this->userStore->getById($userId);
if (empty($user)) {
throw new NotFoundException(Lang::get('user_n_not_found', $userId));
}
$this->layout->title = $user->getName();
$this->layout->subtitle = Lang::get('edit_user');
$values = array_merge($user->getDataArray(), $this->getParams());
$form = $this->userForm($values, 'edit/' . $userId);
if ($method != 'POST' || ($method == 'POST' && !$form->validate())) {
$view = new b8\View('UserForm');
$view->type = 'edit';
$view->user = $user;
$view->form = $form;
return $view->render();
}
$name = $this->getParam('name', null);
$email = $this->getParam('email', null);
$password = $this->getParam('password', null);
$isAdmin = (int)$this->getParam('is_admin', 0);
$this->userService->updateUser($user, $name, $email, $password, $isAdmin);
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL . 'user');
return $response;
}
/**
* Create user add / edit form.
*/
protected function userForm($values, $type = 'add')
{
$form = new Form();
$form->setMethod('POST');
$form->setAction(PHPCI_URL.'user/' . $type);
$form->addField(new Form\Element\Csrf('csrf'));
$field = new Form\Element\Email('email');
$field->setRequired(true);
$field->setLabel(Lang::get('email_address'));
$field->setClass('form-control');
$field->setContainerClass('form-group');
$form->addField($field);
$field = new Form\Element\Text('name');
$field->setRequired(true);
$field->setLabel(Lang::get('name'));
$field->setClass('form-control');
$field->setContainerClass('form-group');
$form->addField($field);
$field = new Form\Element\Password('password');
if ($type == 'add') {
$field->setRequired(true);
$field->setLabel(Lang::get('password'));
} else {
$field->setRequired(false);
$field->setLabel(Lang::get('password_change'));
}
$field->setClass('form-control');
$field->setContainerClass('form-group');
$form->addField($field);
$field = new Form\Element\Checkbox('is_admin');
$field->setRequired(false);
$field->setCheckedValue(1);
$field->setLabel(Lang::get('is_user_admin'));
$field->setContainerClass('form-group');
$form->addField($field);
$field = new Form\Element\Submit();
$field->setValue(Lang::get('save_user'));
$field->setClass('btn-success');
$form->addField($field);
$form->setValues($values);
return $form;
}
/**
* Delete a user.
*/
public function delete($userId)
{
$this->requireAdmin();
$user = $this->userStore->getById($userId);
if (empty($user)) {
throw new NotFoundException(Lang::get('user_n_not_found', $userId));
}
$this->userService->deleteUser($user);
$response = new b8\Http\Response\RedirectResponse();
$response->setHeader('Location', PHPCI_URL . 'user');
return $response;
}
}

View file

@ -0,0 +1,453 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014-2015, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Controller;
use b8;
use b8\Store;
use Exception;
use PHPCI\BuildFactory;
use PHPCI\Model\Project;
use PHPCI\Service\BuildService;
use PHPCI\Store\BuildStore;
use PHPCI\Store\ProjectStore;
/**
* Webhook Controller - Processes webhook pings from BitBucket, Github, Gitlab, etc.
*
* @author Dan Cryer <dan@block8.co.uk>
* @author Sami Tikka <stikka@iki.fi>
* @author Alex Russell <alex@clevercherry.com>
* @author Guillaume Perréal <adirelle@gmail.com>
* @package PHPCI
* @subpackage Web
*
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
*/
class WebhookController extends \b8\Controller
{
/**
* @var BuildStore
*/
protected $buildStore;
/**
* @var ProjectStore
*/
protected $projectStore;
/**
* @var BuildService
*/
protected $buildService;
/**
* Initialise the controller, set up stores and services.
*/
public function init()
{
$this->buildStore = Store\Factory::getStore('Build');
$this->projectStore = Store\Factory::getStore('Project');
$this->buildService = new BuildService($this->buildStore);
}
/** Handle the action, Ensuring to return a JsonResponse.
*
* @param string $action
* @param mixed $actionParams
*
* @return \b8\Http\Response
*/
public function handleAction($action, $actionParams)
{
$response = new b8\Http\Response\JsonResponse();
try {
$data = parent::handleAction($action, $actionParams);
if (isset($data['responseCode'])) {
$response->setResponseCode($data['responseCode']);
unset($data['responseCode']);
}
$response->setContent($data);
} catch (Exception $ex) {
$response->setResponseCode(500);
$response->setContent(array('status' => 'failed', 'error' => $ex->getMessage()));
}
return $response;
}
/**
* Called by Bitbucket.
*/
public function bitbucket($projectId)
{
$project = $this->fetchProject($projectId, 'bitbucket');
// Support both old services and new webhooks
if ($payload = $this->getParam('payload')) {
return $this->bitbucketService(json_decode($payload, true), $project);
}
$payload = json_decode(file_get_contents("php://input"), true);
if (empty($payload['push']['changes'])) {
// Invalid event from bitbucket
return [
'status' => 'failed',
'commits' => []
];
}
return $this->bitbucketWebhook($payload, $project);
}
/**
* Bitbucket webhooks.
*/
protected function bitbucketWebhook($payload, $project)
{
$results = array();
$status = 'failed';
foreach ($payload['push']['changes'] as $commit) {
try {
$email = $commit['new']['target']['author']['raw'];
$email = substr($email, 0, strpos($email, '>'));
$email = substr($email, strpos($email, '<') + 1);
$results[$commit['new']['target']['hash']] = $this->createBuild(
$project,
$commit['new']['target']['hash'],
$commit['new']['name'],
$email,
$commit['new']['target']['message']
);
$status = 'ok';
} catch (Exception $ex) {
$results[$commit['new']['target']['hash']] = array('status' => 'failed', 'error' => $ex->getMessage());
}
}
return array('status' => $status, 'commits' => $results);
}
/**
* Bitbucket POST service.
*/
protected function bitbucketService($payload, $project)
{
$payload = json_decode($this->getParam('payload'), true);
$results = array();
$status = 'failed';
foreach ($payload['commits'] as $commit) {
try {
$email = $commit['raw_author'];
$email = substr($email, 0, strpos($email, '>'));
$email = substr($email, strpos($email, '<') + 1);
$results[$commit['raw_node']] = $this->createBuild(
$project,
$commit['raw_node'],
$commit['branch'],
$email,
$commit['message']
);
$status = 'ok';
} catch (Exception $ex) {
$results[$commit['raw_node']] = array('status' => 'failed', 'error' => $ex->getMessage());
}
}
return array('status' => $status, 'commits' => $results);
}
/**
* Called by POSTing to /webhook/git/<project_id>?branch=<branch>&commit=<commit>
*
* @param string $projectId
*/
public function git($projectId)
{
$project = $this->fetchProject($projectId, array('local', 'remote'));
$branch = $this->getParam('branch', $project->getBranch());
$commit = $this->getParam('commit');
$commitMessage = $this->getParam('message');
$committer = $this->getParam('committer');
return $this->createBuild($project, $commit, $branch, $committer, $commitMessage);
}
/**
* Called by Github Webhooks:
*/
public function github($projectId)
{
$project = $this->fetchProject($projectId, 'github');
switch ($_SERVER['CONTENT_TYPE']) {
case 'application/json':
$payload = json_decode(file_get_contents('php://input'), true);
break;
case 'application/x-www-form-urlencoded':
$payload = json_decode($this->getParam('payload'), true);
break;
default:
return array('status' => 'failed', 'error' => 'Content type not supported.', 'responseCode' => 401);
}
// Handle Pull Request web hooks:
if (array_key_exists('pull_request', $payload)) {
return $this->githubPullRequest($project, $payload);
}
// Handle Push web hooks:
if (array_key_exists('commits', $payload)) {
return $this->githubCommitRequest($project, $payload);
}
return array('status' => 'ignored', 'message' => 'Unusable payload.');
}
/**
* Handle the payload when Github sends a commit webhook.
*
* @param Project $project
* @param array $payload
* @param b8\Http\Response\JsonResponse $response
*
* @return b8\Http\Response\JsonResponse
*/
protected function githubCommitRequest(Project $project, array $payload)
{
// Github sends a payload when you close a pull request with a
// non-existent commit. We don't want this.
if (array_key_exists('after', $payload) && $payload['after'] === '0000000000000000000000000000000000000000') {
return array('status' => 'ignored');
}
if (isset($payload['commits']) && is_array($payload['commits'])) {
// If we have a list of commits, then add them all as builds to be tested:
$results = array();
$status = 'failed';
foreach ($payload['commits'] as $commit) {
if (!$commit['distinct']) {
$results[$commit['id']] = array('status' => 'ignored');
continue;
}
try {
$branch = str_replace('refs/heads/', '', $payload['ref']);
$committer = $commit['committer']['email'];
$results[$commit['id']] = $this->createBuild(
$project,
$commit['id'],
$branch,
$committer,
$commit['message']
);
$status = 'ok';
} catch (Exception $ex) {
$results[$commit['id']] = array('status' => 'failed', 'error' => $ex->getMessage());
}
}
return array('status' => $status, 'commits' => $results);
}
if (substr($payload['ref'], 0, 10) == 'refs/tags/') {
// If we don't, but we're dealing with a tag, add that instead:
$branch = str_replace('refs/tags/', 'Tag: ', $payload['ref']);
$committer = $payload['pusher']['email'];
$message = $payload['head_commit']['message'];
return $this->createBuild($project, $payload['after'], $branch, $committer, $message);
}
return array('status' => 'ignored', 'message' => 'Unusable payload.');
}
/**
* Handle the payload when Github sends a Pull Request webhook.
*
* @param Project $project
* @param array $payload
*/
protected function githubPullRequest(Project $project, array $payload)
{
// We only want to know about open pull requests:
if (!in_array($payload['action'], array('opened', 'synchronize', 'reopened'))) {
return array('status' => 'ok');
}
$headers = array();
$token = \b8\Config::getInstance()->get('phpci.github.token');
if (!empty($token)) {
$headers[] = 'Authorization: token ' . $token;
}
$url = $payload['pull_request']['commits_url'];
$http = new \b8\HttpClient();
$http->setHeaders($headers);
$response = $http->get($url);
// Check we got a success response:
if (!$response['success']) {
throw new Exception('Could not get commits, failed API request.');
}
$results = array();
$status = 'failed';
foreach ($response['body'] as $commit) {
// Skip all but the current HEAD commit ID:
$id = $commit['sha'];
if ($id != $payload['pull_request']['head']['sha']) {
$results[$id] = array('status' => 'ignored', 'message' => 'not branch head');
continue;
}
try {
$branch = str_replace('refs/heads/', '', $payload['pull_request']['base']['ref']);
$committer = $commit['commit']['author']['email'];
$message = $commit['commit']['message'];
$remoteUrlKey = $payload['pull_request']['head']['repo']['private'] ? 'ssh_url' : 'clone_url';
$extra = array(
'build_type' => 'pull_request',
'pull_request_id' => $payload['pull_request']['id'],
'pull_request_number' => $payload['number'],
'remote_branch' => $payload['pull_request']['head']['ref'],
'remote_url' => $payload['pull_request']['head']['repo'][$remoteUrlKey],
);
$results[$id] = $this->createBuild($project, $id, $branch, $committer, $message, $extra);
$status = 'ok';
} catch (Exception $ex) {
$results[$id] = array('status' => 'failed', 'error' => $ex->getMessage());
}
}
return array('status' => $status, 'commits' => $results);
}
/**
* Called by Gitlab Webhooks:
*/
public function gitlab($projectId)
{
$project = $this->fetchProject($projectId, 'gitlab');
$payloadString = file_get_contents("php://input");
$payload = json_decode($payloadString, true);
// build on merge request events
if (isset($payload['object_kind']) && $payload['object_kind'] == 'merge_request') {
$attributes = $payload['object_attributes'];
if ($attributes['state'] == 'opened' || $attributes['state'] == 'reopened') {
$branch = $attributes['source_branch'];
$commit = $attributes['last_commit'];
$committer = $commit['author']['email'];
return $this->createBuild($project, $commit['id'], $branch, $committer, $commit['message']);
}
}
// build on push events
if (isset($payload['commits']) && is_array($payload['commits'])) {
// If we have a list of commits, then add them all as builds to be tested:
$results = array();
$status = 'failed';
foreach ($payload['commits'] as $commit) {
try {
$branch = str_replace('refs/heads/', '', $payload['ref']);
$committer = $commit['author']['email'];
$results[$commit['id']] = $this->createBuild(
$project,
$commit['id'],
$branch,
$committer,
$commit['message']
);
$status = 'ok';
} catch (Exception $ex) {
$results[$commit['id']] = array('status' => 'failed', 'error' => $ex->getMessage());
}
}
return array('status' => $status, 'commits' => $results);
}
return array('status' => 'ignored', 'message' => 'Unusable payload.');
}
/**
* Wrapper for creating a new build.
*
* @param Project $project
* @param string $commitId
* @param string $branch
* @param string $committer
* @param string $commitMessage
* @param array $extra
*
* @return array
*
* @throws Exception
*/
protected function createBuild(
Project $project,
$commitId,
$branch,
$committer,
$commitMessage,
array $extra = null
) {
// Check if a build already exists for this commit ID:
$builds = $this->buildStore->getByProjectAndCommit($project->getId(), $commitId);
if ($builds['count']) {
return array(
'status' => 'ignored',
'message' => sprintf('Duplicate of build #%d', $builds['items'][0]->getId())
);
}
// If not, create a new build job for it:
$build = $this->buildService->createBuild($project, $commitId, $branch, $committer, $commitMessage, $extra);
return array('status' => 'ok', 'buildID' => $build->getID());
}
/**
* Fetch a project and check its type.
*
* @param int $projectId
* @param array|string $expectedType
*
* @return Project
*
* @throws Exception If the project does not exist or is not of the expected type.
*/
protected function fetchProject($projectId, $expectedType)
{
$project = $this->projectStore->getById($projectId);
if (empty($projectId)) {
throw new Exception('Project does not exist: ' . $projectId);
}
if (is_array($expectedType)
? !in_array($project->getType(), $expectedType)
: $project->getType() !== $expectedType
) {
throw new Exception('Wrong project type: ' . $project->getType());
}
return $project;
}
}

View file

@ -0,0 +1,68 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI;
/**
* Error Handler
*
* @package PHPCI\Logging
*/
class ErrorHandler
{
/**
* @var array
*/
protected $levels = array(
E_WARNING => 'Warning',
E_NOTICE => 'Notice',
E_USER_ERROR => 'User Error',
E_USER_WARNING => 'User Warning',
E_USER_NOTICE => 'User Notice',
E_STRICT => 'Runtime Notice',
E_RECOVERABLE_ERROR => 'Catchable Fatal Error',
E_DEPRECATED => 'Deprecated',
E_USER_DEPRECATED => 'User Deprecated',
);
/**
* Registers an instance of the error handler to throw ErrorException.
*/
public static function register()
{
$handler = new static();
set_error_handler(array($handler, 'handleError'));
}
/**
* @param integer $level
* @param string $message
* @param string $file
* @param integer $line
*
* @throws \ErrorException
*
* @internal
*/
public function handleError($level, $message, $file, $line)
{
if (error_reporting() & $level === 0) {
return;
}
$exceptionLevel = isset($this->levels[$level]) ? $this->levels[$level] : $level;
throw new \ErrorException(
sprintf('%s: %s in %s line %d', $exceptionLevel, $message, $file, $line),
0,
$level,
$file,
$line
);
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Helper;
use SensioLabs\AnsiConverter\AnsiToHtmlConverter;
/**
* Converts ANSI output to HTML.
*
* @package PHPCI\Helper
*/
final class AnsiConverter
{
static private $converter = null;
/**
* Initialize the singleton.
*
* @return AnsiToHtmlConverter
*/
private static function getInstance()
{
if (self::$converter === null) {
self::$converter = new AnsiToHtmlConverter(null, false);
}
return self::$converter;
}
/**
* Convert a text containing ANSI color sequences into HTML code.
*
* @param string $text The text to convert
*
* @return string The HTML code.
*/
public static function convert($text)
{
return self::getInstance()->convert($text);
}
/**
* Do not instantiate this class.
*/
private function __construct()
{
}
}

View file

@ -0,0 +1,227 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Helper;
use Exception;
use PHPCI\Logging\BuildLogger;
use Psr\Log\LogLevel;
/**
* Handles running system commands with variables.
* @package PHPCI\Helper
*/
abstract class BaseCommandExecutor implements CommandExecutor
{
/**
* @var BuildLogger
*/
protected $logger;
/**
* @var bool
*/
protected $quiet;
/**
* @var bool
*/
protected $verbose;
protected $lastOutput;
protected $lastError;
public $logExecOutput = true;
/**
* The path which findBinary will look in.
* @var string
*/
protected $rootDir;
/**
* Current build path
* @var string
*/
protected $buildPath;
/**
* @param BuildLogger $logger
* @param string $rootDir
* @param bool $quiet
* @param bool $verbose
*/
public function __construct(BuildLogger $logger, $rootDir, &$quiet = false, &$verbose = false)
{
$this->logger = $logger;
$this->quiet = $quiet;
$this->verbose = $verbose;
$this->lastOutput = array();
$this->rootDir = $rootDir;
}
/**
* Executes shell commands.
* @param array $args
* @return bool Indicates success
*/
public function executeCommand($args = array())
{
$this->lastOutput = array();
$command = call_user_func_array('sprintf', $args);
$this->logger->logDebug($command);
if ($this->quiet) {
$this->logger->log('Executing: ' . $command);
}
$status = 0;
$descriptorSpec = array(
0 => array("pipe", "r"), // stdin
1 => array("pipe", "w"), // stdout
2 => array("pipe", "w"), // stderr
);
$pipes = array();
$process = proc_open($command, $descriptorSpec, $pipes, $this->buildPath, null);
if (is_resource($process)) {
fclose($pipes[0]);
$this->lastOutput = stream_get_contents($pipes[1]);
$this->lastError = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$status = proc_close($process);
}
$this->lastOutput = array_filter(explode(PHP_EOL, $this->lastOutput));
$shouldOutput = ($this->logExecOutput && ($this->verbose || $status != 0));
if ($shouldOutput && !empty($this->lastOutput)) {
$this->logger->log($this->lastOutput);
}
if (!empty($this->lastError)) {
$this->logger->log("\033[0;31m" . $this->lastError . "\033[0m", LogLevel::ERROR);
}
$rtn = false;
if ($status == 0) {
$rtn = true;
}
return $rtn;
}
/**
* Returns the output from the last command run.
*/
public function getLastOutput()
{
return implode(PHP_EOL, $this->lastOutput);
}
/**
* Returns the stderr output from the last command run.
*/
public function getLastError()
{
return $this->lastError;
}
/**
* Find a binary required by a plugin.
* @param string $binary
* @param bool $quiet
* @return null|string
*/
public function findBinary($binary, $quiet = false)
{
$composerBin = $this->getComposerBinDir(realpath($this->buildPath));
if (is_string($binary)) {
$binary = array($binary);
}
foreach ($binary as $bin) {
$this->logger->log(Lang::get('looking_for_binary', $bin), LogLevel::DEBUG);
if (is_dir($composerBin) && is_file($composerBin.'/'.$bin)) {
$this->logger->log(Lang::get('found_in_path', $composerBin, $bin), LogLevel::DEBUG);
return $composerBin . '/' . $bin;
}
if (is_file($this->rootDir . $bin)) {
$this->logger->log(Lang::get('found_in_path', 'root', $bin), LogLevel::DEBUG);
return $this->rootDir . $bin;
}
if (is_file($this->rootDir . 'vendor/bin/' . $bin)) {
$this->logger->log(Lang::get('found_in_path', 'vendor/bin', $bin), LogLevel::DEBUG);
return $this->rootDir . 'vendor/bin/' . $bin;
}
$findCmdResult = $this->findGlobalBinary($bin);
if (is_file($findCmdResult)) {
$this->logger->log(Lang::get('found_in_path', '', $bin), LogLevel::DEBUG);
return $findCmdResult;
}
}
if ($quiet) {
return;
}
throw new Exception(Lang::get('could_not_find', implode('/', $binary)));
}
/**
* Find a binary which is installed globally on the system
* @param string $binary
* @return null|string
*/
abstract protected function findGlobalBinary($binary);
/**
* Try to load the composer.json file in the building project
* If the bin-dir is configured, return the full path to it
* @param string $path Current build path
* @return string|null
*/
public function getComposerBinDir($path)
{
if (is_dir($path)) {
$composer = $path.'/composer.json';
if (is_file($composer)) {
$json = json_decode(file_get_contents($composer));
if (isset($json->config->{"bin-dir"})) {
return $path.'/'.$json->config->{"bin-dir"};
} elseif (is_dir($path . '/vendor/bin')) {
return $path . '/vendor/bin';
}
}
}
return null;
}
/**
* Set the buildPath property.
* @param string $path
*/
public function setBuildPath($path)
{
$this->buildPath = $path;
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Helper;
/**
* User Helper - Provides access to logged in user information in views.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Web
*/
class Build
{
/**
* Returns a more human-friendly version of a plugin name.
* @param $name
* @return mixed
*/
public function formatPluginName($name)
{
return str_replace('Php', 'PHP', ucwords(str_replace('_', ' ', $name)));
}
}

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