fannie/classlib2.0/FannieAPI.php
<?php
/*******************************************************************************
Copyright 2013 Whole Foods Co-op
This file is part of CORE-POS.
IT CORE is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
IT CORE is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
in the file license.txt along with IT CORE; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*********************************************************************************/
class FannieAPI
{
/**
Initialize session to retain class
definition info.
*/
static public function init()
{
$session = self::session();
if (!isset($session->FannieClassMap)) {
$session->FannieClassMap = array();
} elseif (!is_array($session->FannieClassMap)) {
$session->FannieClassMap = array();
}
$map = $session->FannieClassMap;
if (!isset($map['SQLManager'])) {
$map['SQLManager'] = realpath(dirname(__FILE__).'/../src/SQLManager.php');
}
if (!isset($map['FPDF'])) {
$map['FPDF'] = realpath(dirname(__FILE__).'/../src/fpdf/fpdf.php');
}
$session->FannieClassMap = $map;
}
static private $namedSession = null;
static private function session()
{
if (self::$namedSession === null) {
if (!class_exists('COREPOS\\common\\NamedSession', false)) {
include(__DIR__ . '/../../common/NamedSession.php');
}
$path = realpath(__DIR__ . '/../');
self::$namedSession = new COREPOS\common\NamedSession($path);
}
return self::$namedSession;
}
/**
Idea: use one shared file w/ map of class
names to definition files. Manage file locking
more granularly than with sessions.
*/
static private function mapFile()
{
return sys_get_temp_dir() . '/fannie.class.map';
}
/**
Get the map from cache.
Acquires a shared lock to read the file.
*/
static private function getCachedMap()
{
$fn = self::mapFile();
$fp = fopen($fn, 'r');
if (flock($fp, LOCK_SH)) {
$map = file_get_contents($fn);
flock($fp, LOCK_UN);
fclose($fp);
$map = json_decode($map, true);
if (is_array($map)) {
return $map;
} else {
return self::initMap();
}
} else {
fclose($fp);
return self::initMap();
}
}
/**
Initialize shared map if it does not exist
*/
static private function initMap()
{
$map = array('FannieClassMap' => array());
$map['FannieClassMap']['SQLManager'] = realpath(dirname(__FILE__).'/../src/SQLManager.php');
$map['FannieClassMap']['FPDF'] = realpath(dirname(__FILE__).'/../src/fpdf/fpdf.php');
return self::writeCachedMap($map);
}
/**
Write updated map to cache.
Acquires an exclusive lock to write the file.
*/
static private function writeCachedMap($map)
{
$fn = self::mapFile();
$fp = fopen($fn, 'a');
if (flock($fp, LOCK_EX)) {
ftruncate($fp, 0);
fwrite($fp, json_encode($map));
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
return true;
} else {
fclose($fp);
return false;
}
}
/**
Load definition for given class
@param $name the class name
*/
static public function loadClass($name)
{
$session = self::session();
$map = isset($session->FannieClassMap) ? $session->FannieClassMap : array();
// class map should be array
// of class_name => file_name
if (!is_array($map)) {
$map = array();
$map['SQLManager'] = realpath(dirname(__FILE__).'/../src/SQLManager.php');
$map['FPDF'] = realpath(dirname(__FILE__).'/../src/fpdf/fpdf.php');
$session->FannieClassMap = $map;
}
// if class is known in the map, include its file
// otherwise search for an appropriate file
if (isset($map[$name]) && !class_exists($name,false)
&& file_exists($map[$name])) {
include_once($map[$name]);
} else {
/**
There's a namespace involved
If the prefix is COREPOS\Fannie\API, look for class
in classlib2.0 directory path.
If the prefix is COREPOS\Fannie\Plugin, look for class
in modules/plugins2.0 directory path
Otherwise, just strip off the namespace and search
both plugins and API class library
*/
$real_name = $name;
if (strstr($name, '\\')) {
$found = self::loadFromNamespace($name);
if ($found) {
include_once($found);
$map[$name] = $found;
$session->FannieClassMap = $map;
}
return;
}
// search class lib for definition
$file = self::findClass($name, dirname(__FILE__));
if ($file !== false) {
include_once($file);
return;
}
// search plugins for definition
$file = self::findClass($name, dirname(__FILE__).'/../modules/plugins2.0');
if ($file !== false) {
// only use if enabled
$owner = \COREPOS\Fannie\API\FanniePlugin::memberOf($file);
if (\COREPOS\Fannie\API\FanniePlugin::isEnabled($owner)) {
include_once($file);
return;
}
}
}
}
static private function loadFromNamespace($class)
{
$namespaces = array(
'COREPOS\\common\\' => dirname(__FILE__) . '/../../common/',
'COREPOS\\Fannie\\API\\' => dirname(__FILE__) . '/',
'COREPOS\\Fannie\\Plugin\\' => dirname(__FILE__) . '/../modules/plugins2.0/',
);
$poser = FannieConfig::config('POSER');
if ($poser) {
$namespaces['Poser\\'] = $poser;
}
$class = ltrim($class, '\\');
foreach ($namespaces as $namespace => $path) {
if (substr($class, 0, strlen($namespace)) == $namespace) {
$file = $path . str_replace('\\', DIRECTORY_SEPARATOR, substr($class, strlen($namespace))) . '.php';
if (file_exists($file)) {
return $file;
} else {
throw new Exception('Invalid class in COREPOS namespace: ' . $class);
}
}
}
return false;
}
/**
Search for class in given path
@param $name the class name
@param $path path to search
@return A filename or false
*/
static private function findClass($name, $path)
{
if (!is_dir($path)) {
return false;
} else if ($name == 'index') {
return false;
}
$session = self::session();
$map = $session->FannieClassMap;
$dir = opendir($path);
$ret = false;
while ($dir && ($file=readdir($dir)) !== false) {
$fullname = realpath($path.'/'.$file);
if ($file[0] != '.' && $file != 'noauto' && $file != 'node_modules' && is_dir($fullname)) {
// recurse looking for file
$file = self::findClass($name, $fullname);
$map = array_merge($map, $session->FannieClassMap);
if ($file !== false) {
$ret = $file;
break;
}
} elseif (substr($file,-4) == '.php') {
// map all PHP files as long as we're searching
// but only return if the correct file is found
$class = substr($file,0,strlen($file)-4);
$map[$class] = $fullname;
if ($class == $name) {
$ret = $fullname;
break;
}
}
}
$session->FannieClassMap = $map;
return $ret;
}
static public function listFiles($path)
{
if (is_file($path) && substr($path,-4)=='.php') {
return array($path);
} elseif (is_dir($path)) {
$dir = opendir($path);
$ret = array();
$exclude = array('noauto', 'index.php', 'Store-Specific');
while (($file=readdir($dir)) !== false) {
if ($file[0] != '.' && !in_array($file, $exclude)) {
$ret = array_merge($ret, self::listFiles($path.'/'.$file));
}
}
return $ret;
}
return array();
}
static private function searchDirectories($base_class)
{
$directories = array();
// prefer directories from Poser if present
$poser = FannieConfig::config('POSER');
if ($poser) {
$directories[] = "$poser/office_plugins/";
}
$directories[] = dirname(__FILE__).'/../modules/plugins2.0/';
// leading backslash is ignored
if ($base_class[0] == '\\') {
$base_class = substr($base_class, 1);
}
switch($base_class) {
case 'COREPOS\Fannie\API\item\ItemModule':
case 'COREPOS\Fannie\API\item\ItemRow':
$directories[] = dirname(__FILE__).'/../item/modules/';
break;
case 'COREPOS\Fannie\API\member\MemberModule':
$directories[] = dirname(__FILE__).'/../mem/modules/';
break;
case 'FannieTask':
$directories[] = dirname(__FILE__).'/../cron/tasks/';
break;
case 'BasicModel':
$directories[] = dirname(__FILE__).'/data/models/';
break;
case 'COREPOS\Fannie\API\data\hooks\BasicModelHook':
$directories[] = dirname(__FILE__).'/data/hooks/';
break;
case 'FannieReportPage':
$directories[] = dirname(__FILE__).'/../reports/';
$directories[] = dirname(__FILE__).'/../purchasing/reports/';
break;
case 'COREPOS\Fannie\API\FannieReportTool':
$directories[] = dirname(__FILE__).'/../reports/';
break;
case 'COREPOS\Fannie\API\item\FannieSignage':
$directories[] = dirname(__FILE__) . '/item/signage/';
break;
case 'COREPOS\Fannie\API\item\PriceRounder':
$directories[] = dirname(__FILE__) . '/item/';
break;
case 'COREPOS\Fannie\API\monitor\Monitor':
$directories[] = dirname(__FILE__) . '/monitor/';
break;
case 'COREPOS\Fannie\API\data\SyncSpecial':
$directories[] = dirname(__FILE__) . '/data/lanesync/';
break;
case 'FanniePage':
$directories[] = dirname(__FILE__).'/../admin/';
$directories[] = dirname(__FILE__).'/../batches/';
$directories[] = dirname(__FILE__).'/../cron/management/';
$directories[] = dirname(__FILE__).'/../item/';
$directories[] = dirname(__FILE__).'/../logs/';
$directories[] = dirname(__FILE__).'/../reports/';
$directories[] = dirname(__FILE__).'/../mem/';
$directories[] = dirname(__FILE__).'/../ordering/';
$directories[] = dirname(__FILE__).'/../purchasing/';
/*
$directories[] = dirname(__FILE__).'/../install/';
*/
break;
}
return $directories;
}
/**
Get a list of all available classes implementing a given
base class
@param $base_class [string] name of base class
@param $include_base [boolean] include base class name in the result set
[optional, default false]
@return [array] of [string] class names
*/
static public function listModules($base_class, $include_base=false, $debug=false)
{
// leading backslash is ignored
if ($base_class[0] == '\\') {
$base_class = substr($base_class, 1);
}
$directories = self::searchDirectories($base_class);
// recursive search
$search = function($path, $depth) use (&$search) {
if (is_file($path) && substr($path,-4)=='.php') {
return array($path);
} elseif (is_dir($path) && $depth < 10) {
$dh = opendir($path);
$ret = array();
while( ($file=readdir($dh)) !== false) {
if ($file == '.' || $file == '..') continue;
if ($file == 'noauto') continue;
if ($file == 'node_modules') continue;
if ($file == 'index.php') continue;
if ($file == 'Store-Specific') continue;
$ret = array_merge($ret, $search($path.'/'.$file, $depth+1));
}
return $ret;
}
return array();
};
$files = array_reduce($directories,
function ($carry, $dir) use ($search) { return array_merge($carry, $search($dir, 0)); },
array()
);
if ($debug) {
return $files;
}
$ret = array();
foreach($files as $file) {
$class = substr(basename($file),0,strlen(basename($file))-4);
// matched base class
if ($class === $base_class) {
if ($include_base) {
$ret[] = $class;
}
continue;
}
// almost certainly not a class definition
if ($class == 'index') {
continue;
}
// if the file is part of a plugin, make sure
// the plugin is enabled. The exception is when requesting
// a list of plugin classes
if (strstr($file, 'plugins2.0') && $base_class != 'FanniePlugin' && $base_class != 'COREPOS\Fannie\API\FanniePlugin') {
$parent = \COREPOS\Fannie\API\FanniePlugin::memberOf($file);
if ($parent === false || !\COREPOS\Fannie\API\FanniePlugin::isEnabled($parent)) {
continue;
}
}
// verify class exists
ob_start();
include_once($file);
ob_end_clean();
$namespaced_class = self::pathToClass($file);
if (!class_exists($class, false) && !class_exists($namespaced_class, false)) {
continue;
}
if (class_exists($class, false) && is_subclass_of($class, $base_class)) {
$ret[] = $class;
} elseif (class_exists($namespaced_class, false) && is_subclass_of($namespaced_class, $base_class)) {
$ret[] = $namespaced_class;
} elseif (class_exists($namespaced_class, false) && $namespaced_class == $base_class && $include_base) {
$ret[] = $namespaced_class;
}
}
return $ret;
}
/**
Determine fully namespaced class name
based on filesystem path
@param $path [string] file system path
@return [string] class name with namespace if applicable
*/
public static function pathToClass($path)
{
$name = false;
$basedir = 'unknown';
$path = realpath($path);
$path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
if (strstr($path, '/modules/plugins2.0/')) {
$name = 'COREPOS\\Fannie\\Plugin';
$basedir = 'plugins2.0';
} elseif (strstr($path, '/classlib2.0/')) {
$name = 'COREPOS\\Fannie\\API';
$basedir = 'classlib2.0';
}
if ($name) {
$parts = explode('/', $path);
$start = false;
for ($i=0; $i<count($parts); $i++) {
if (!$start && $parts[$i] == $basedir) {
$start = true;
} elseif ($start) {
$name .= '\\' . $parts[$i];
}
}
} else {
$name = basename($path);
}
return substr($name, 0, strlen($name)-4);
}
}
FannieAPI::init();
spl_autoload_register(array('FannieAPI','loadClass'), true, true);
if (file_exists(dirname(__FILE__) . '/../../vendor/autoload.php')) {
include_once(dirname(__FILE__) . '/../../vendor/autoload.php');
}