5 ini_set(
'display_errors',
false);
6 error_reporting(E_ALL);
20 const EVENT_HALT =
'fred:halt';
21 const EVENT_EXTERNAL_ERROR =
'fred:externalError';
22 const EVENT_SHUTDOWN =
'fred:shutdown';
24 public $debug =
false;
25 public $autoloadCacheKey =
'fred:autoloadCache';
26 public $autoloadMissingKey =
'fred:autoloadMissing';
27 public $defaultComponentNamespace = __CLASS__;
30 public $templatePath = __DIR__ .
'/../../template/';
31 public $version = null;
32 public $ignoreErrors =
'/^SOAP-ERROR/';
33 public $stripObjectsMemoryLimit = null;
34 public $stripObjectsMaxDepth = 10;
36 protected $_startTime = null;
37 protected $_initialized =
false;
38 protected $_config = [];
39 protected $_internalError =
false;
40 protected $_errorHash = null;
41 protected $_errorBacktrace = null;
42 protected $_timeLimit = null;
44 protected $_autoloadNamespaces = [];
45 protected $_autoloadClasses = [];
46 protected $_autoloadFiles = [];
47 protected $_autoloadCache = [];
48 protected $_autoloadCacheLimit = 250;
49 protected $_autoloadMissing = [];
50 protected $_sharedCacheFile = null;
51 protected $_sharedCacheTime = null;
52 protected $_sharedCacheTtl = 600;
53 protected $_sharedCacheTtlSpread = 10;
54 protected $_components = [];
56 protected $_releaseNotesFile = __DIR__ .
'/../../doc/pages/notes.php';
57 protected $_releaseNotesKey =
'fred:releaseNotesTime';
59 protected $_maintenanceTime = null;
60 protected $_maintenanceMessage = [];
64 protected $_maintenanceKey =
'fred:maintenanceMessage';
72 $this->_startTime = $_SERVER[
'REQUEST_TIME_FLOAT'] ?? microtime(
true);
73 $this->_config = is_array($config) ? $config : require($config);
74 spl_autoload_register([$this,
'autoload'],
true,
true);
78 $this->exceptionHandler($e);
85 $this->configure($this->_config);
86 ini_set(
'display_errors',$this->debug);
87 set_error_handler([$this,
'errorHandler']);
88 set_exception_handler([$this,
'exceptionHandler']);
89 register_shutdown_function([$this,
'shutdownFunction']);
90 $this->publish([
'startTime' => self::READABLE,
'autoloadNamespaces' => self::READABLE,
'autoloadClasses' => self::READABLE]);
92 if(session_status() == PHP_SESSION_NONE) session_start();
93 if($this->_sharedCacheFile)
try{
94 include($this->_sharedCacheFile);
95 if((($delta = $this->_startTime - $this->_sharedCacheTime - $this->_sharedCacheTtl) > 0) && ($delta > rand(0,1000) / 1000 * $this->_sharedCacheTtlSpread)){
96 $this->_autoloadCache = $this->_autoloadMissing = [];
97 $this->_sharedCacheTime = null;
100 catch(\Throwable $e){
101 \Rsi\File::unlink($this->_sharedCacheFile);
102 $this->log->info($e);
104 if(array_key_exists($this->autoloadCacheKey,$_SESSION))
105 $this->_autoloadCache = array_merge($this->_autoloadCache,$_SESSION[$this->autoloadCacheKey]);
106 if(array_key_exists($this->autoloadMissingKey,$_SESSION))
107 $this->_autoloadMissing = array_merge($this->_autoloadMissing,$_SESSION[$this->autoloadMissingKey]);
109 catch(\Exception $e){
110 if($this->debug)
throw $e;
113 $this->log->debug(
'FRED™ framework initialized',__FILE__,__LINE__);
114 $this->releaseNotes();
116 else $this->maintenance();
117 $this->_initialized =
true;
121 if($this->_initialized && $this->_sharedCacheFile)
try{
122 file_put_contents($temp = $this->_sharedCacheFile . uniqid(
'-',
true) .
'.tmp',
"<?php\n" .
123 '$this->_sharedCacheTime = ' . ($this->_sharedCacheTime ?: $this->_startTime) .
'; //modified ' . date(
'Y-m-d H:i:s') .
"\n" .
124 '$this->_autoloadCache = ' . var_export($this->_autoloadCache,
true) .
";\n" .
125 '$this->_autoloadMissing = ' . var_export(count($this->_autoloadMissing) > $this->_autoloadCacheLimit ? [] : $this->_autoloadMissing,
true) .
';' 127 if(!rename($temp,$this->_sharedCacheFile))
throw new \Exception(
"Could not rename('$temp','{$this->_sharedCacheFile}')");
128 chmod($this->_sharedCacheFile,0666);
130 catch(\Exception $e){
131 $this->log->info($e);
135 catch(\Exception $e){}
138 $_SESSION[$this->autoloadCacheKey] = array_slice($this->_autoloadCache,0,$this->_autoloadCacheLimit,
true);
139 $_SESSION[$this->autoloadMissingKey] = array_slice($this->_autoloadMissing,0,$this->_autoloadCacheLimit);
152 if($this->debug && $this->_initialized) $this->log->debug(__CLASS__ .
"::autoload('$class_name')",__FILE__,__LINE__,[
'className' => $class_name]);
153 if(array_key_exists($class_name,$this->_autoloadCache)) require($this->_autoloadCache[$class_name]);
154 elseif(array_key_exists($class_name,$this->_autoloadClasses))
return require($this->_autoloadClasses[$class_name]);
155 elseif(!in_array($class_name,$this->_autoloadMissing)){
156 $prefix_match =
false;
157 foreach($this->_autoloadNamespaces as $prefix => $paths)
if(substr($class_name,0,strlen($prefix)) == $prefix){
158 $prefix_match =
true;
159 foreach($paths as $path)
if(is_file($filename = $path . str_replace([
'_',
'\\'],
'/',substr($class_name,strlen(rtrim($prefix,
'\\')))) .
'.php')){
160 $this->_autoloadCache[$class_name] = $filename;
161 $this->saveSharedCache();
162 return require($filename);
165 if(!$prefix_match && ($files = $this->_autoloadFiles)){
166 $this->_autoloadFiles =
false;
167 foreach($files as $filename) require($filename);
170 $this->_autoloadMissing[] = $class_name;
171 $this->saveSharedCache();
179 public function halt($status = null){
180 if($this->event->trigger(self::EVENT_HALT,$this,$status) !==
false) exit($status);
189 if(is_object($item))
try{
190 if($item instanceof \Closure){
191 $reflect = new \ReflectionFunction($item);
192 $filename = $reflect->getFileName();
193 $line_no = $reflect->getStartLine();
195 "<closure file='$filename' line='$line_no'>\n" .
196 implode(array_slice(file($filename),$line_no - 1,$reflect->getEndLine() - $line_no + 1)) .
200 if(!$this->stripObjectsMemoryLimit) $this->stripObjectsMemoryLimit = \Rsi::memoryLimit() / 5;
201 if(memory_get_usage() > $this->stripObjectsMemoryLimit) $item =
'out of memory';
202 $objects[$key =
'@@object_' . md5(print_r($item,
true)) .
'@@'] = $item;
206 catch(\Exception $e){
207 $item =
'@@' . $e->getMessage() .
'@@';
209 elseif(is_array($item) && ($level < $this->stripObjectsMaxDepth))
foreach($item as &$sub) $this->stripObjects($sub,$objects,$level + 1);
218 foreach($trace as &$step){
219 if(array_key_exists(
'object',$step)) $this->stripObjects($step[
'object'],$objects);
220 if(array_key_exists(
'args',$step) && $step[
'args']) $this->stripObjects($step[
'args'],$objects);
221 else unset($step[
'args']);
230 print($message .
"\n\n");
231 if(!\Rsi::commandLine()){
232 $trace = debug_backtrace();
234 $this->stripTraceObjects($trace,$objects);
239 $this->halt(
'External error');
241 $this->log->notice(
'External error: ' . $message,$context);
242 if($this->event->trigger(self::EVENT_EXTERNAL_ERROR,$this,$message) !==
false){
243 http_response_code(400);
255 public function internalError($message,$filename = null,$line_no = null,$trace = null){
257 if(ob_get_length()) ob_clean();
259 if($this->debug && (strpos($message,
'Undefined variable: ') === 0)){
260 $this->_internalError =
true;
264 if(!$trace) $trace = debug_backtrace();
265 $this->log->error($message,$filename,$line_no);
266 $this->stripTraceObjects($trace,$objects);
268 if($this->_internalError){
270 if(\Rsi::commandLine()){
271 print(
"$message ($filename:$line_no)\n");
275 print(
"<code>$message ($filename:$line_no)<code><br><br>");
276 $dump = $this->dump();
277 print($dump->head() . $dump->source($filename,$line_no));
278 if($trace) print($dump->var(
'trace',$trace));
279 if($objects) print($dump->var(
'objects',$objects));
282 exit(
'Internal error');
284 $this->_internalError =
true;
286 if(!$this->debug)
try{
287 $this->log->emergency($message,array_filter([
288 'filename' => $filename,
289 'lineNo' => $line_no,
291 'objects' => $objects,
292 'headers' => function_exists(
'getallheaders') ? getallheaders() : null,
293 'args' => $_SERVER[
'argv'] ?? null,
296 'COOKIE' => $_COOKIE,
297 'SESSION' => isset($_SESSION) ? $_SESSION : null
299 usleep(rand(0,10000000));
300 http_response_code(500);
301 if(is_file($template = $this->templatePath .
'error.php')) require($template);
303 catch(\Exception $e){
304 print(
'An unexpected error has occurred');
306 elseif(!\Rsi::commandLine() && is_file($template = $this->templatePath .
'debug.php')) require($template);
307 else print($message . ($filename ?
" ($filename" . ($line_no ?
"@$line_no" : null) .
")" : null) .
"\n\n");
310 catch(\Exception $e){
311 $this->internalError($e->getMessage(),$e->getFile(),$e->getLine(),$e->getTrace());
315 protected function errorHash($message,$filename,$line_no){
316 return md5(
"errorHash($message,$filename,$line_no)");
323 if(error_reporting()){
324 $this->_errorHash = $this->errorHash($message,$filename,$line_no);
325 $this->_errorBacktrace = debug_backtrace();
326 throw new \ErrorException($message,$error_no,0,$filename,$line_no);
328 elseif($this->_initialized) $this->log->info($message,$filename,$line_no);
335 $message = $exception->getMessage();
336 $filename = $exception->getFile();
337 $line_no = $exception->getLine();
338 if(!($exception instanceof \ErrorException) || ($this->errorHash($message,$filename,$line_no) != $this->_errorHash)) $this->_errorBacktrace = null;
339 $this->internalError($message,$filename,$line_no,$this->_errorBacktrace ?: $exception->getTrace());
349 $this->_initialized && !$this->_internalError &&
350 ($error = error_get_last()) && !preg_match($this->ignoreErrors,$error[
'message']) &&
351 ($this->event->trigger(self::EVENT_SHUTDOWN,$this,$error) !==
false)
352 ) $this->internalError($error[
'message'],$error[
'file'],$error[
'line']);
360 list($version,$hash) = explode(
'-',$this->version .
'-',2);
368 $this->_releaseNotesFile &&
369 ($time = File::mtime($this->_releaseNotesFile)) &&
370 ($time != Record::get($_SESSION,$this->_releaseNotesKey)) &&
371 preg_match_all(
'/\\n- ([\\d\\.]+): (.*)/',file_get_contents($this->_releaseNotesFile),$matches,PREG_SET_ORDER)
374 foreach($matches as $match){
375 $version = $match[1] .
'-' . str_replace(
' ',
'-',strtolower(\
Rsi\Str::codeName(crc32($match[2]))));
376 if($this->version == $version) $messages = [];
377 else $messages[] =
"FRED™ version $version: " . str_replace(
'\\\\',
'\\',$match[2]);
380 foreach($messages as $message){
381 $this->message->warning($message);
382 $this->log->warning($message,__FILE__,__LINE__);
384 $this->log->notice(
"Upgraded to FRED™ version $version.",__FILE__,__LINE__);
386 else $_SESSION[$this->_releaseNotesKey] = $time;
393 if($this->_maintenanceTime){
394 if(($delta = (strtotime($this->_maintenanceTime)) - $this->_startTime) <= 0){
395 if(is_file($template = $this->templatePath .
'maintenance.php')) require($template);
396 http_response_code(503);
400 else foreach($this->_maintenanceMessage as $limit => $message){
401 list($limit,$type) = explode(
':',$limit .
':warning');
402 if($delta <= $limit){
403 if($limit != \
Rsi\Record::get($_SESSION,$this->_maintenanceKey)){
404 $this->message->add($type,$message,[
'time' => $this->_maintenanceTime,
'delta' => round($delta / 60)] + compact(
'limit',
'type'));
405 $_SESSION[$this->_maintenanceKey] = $limit;
419 if(!is_array($config))
return $config;
421 foreach($config as $key => $value)
if(substr($key,0,1) ==
'@')
422 $result[substr($key,1)] = (substr($value,0,1) ==
'{') && (substr($value,-1) ==
'}')
423 ? json_decode($this->vars->value(substr($value,1,-1)),
true)
424 : $this->vars->value($value);
425 else $result[$key] = $this->replaceVars($value);
433 $config = $this->config(
'dump');
434 $class_name = $config[
'className'] ??
'Rsi\\Dump';
435 return new $class_name($config);
443 public function config($key,$default = null){
444 return $this->replaceVars(Record::get($this->_config,$key,$default));
448 return $this->defaultComponentNamespace .
'\\' . ucfirst($name);
456 if(!$this->has($name)){
457 $config = $this->config($name);
458 if($config && !is_array($config)){
459 if(is_callable($config)) $config = call_user_func($config,$name);
460 if(is_string($config)) $config = require($config);
462 if(!class_exists($class_name = Record::get($config,
'className') ?: $this->defaultComponentClassName($name)))
463 throw new \Exception(
"Unknown component '$name' ($class_name)");
464 $this->_components[$name] =
new $class_name($this,array_merge([
'name' => $name],$config ?: []));
466 return $this->_components[$name];
473 public function may($name){
474 return array_key_exists($name,$this->_components) || array_key_exists($name,$this->_config) || class_exists($this->defaultComponentClassName($name))
475 ? $this->component($name)
483 public function has($name){
484 return $this->_components[$name] ??
false;
492 if($this->debug) $config[
'debug'] =
true;
493 foreach($this->_components as $name => $component) $config[$name] = $component->clientConfig();
494 return array_filter($config);
498 $this->_autoloadNamespaces = array_merge($this->_autoloadNamespaces,$namespaces);
502 $this->_autoloadClasses = array_merge($this->_autoloadClasses,$classes);
506 $this->_autoloadFiles = array_merge($this->_autoloadFiles,$classes);
510 set_time_limit($value);
511 $this->_timeLimit = microtime(
true) + $value;
515 if($this->_timeLimit === null) $this->_timeLimit = $this->_startTime + ini_get(
'max_execution_time');
516 return max(0,$this->_timeLimit - microtime(
true));
520 return $this->component($key);
523 public function __call($func_name,$params){
524 return call_user_func_array([$this->entity,
'item'],array_merge([$func_name],$params));
has($name)
Get a component if it already exists.
config($key, $default=null)
Get a value from the configuration.
clientConfig()
Public configuration.
init()
Initialize the framework.
defaultComponentClassName($name)
maintenance()
Check for maintenance.
may($name)
Get a component if there is a configuration entry for it.
version(&$hash=null)
Version without hash.
exceptionHandler($exception)
Exception handler.
__construct($config)
Initialize the framework.
__call($func_name, $params)
shutdownFunction()
Shutdown function.
setAutoloadClasses($classes)
Framework for Rapid and Easy Development.
externalError($message, $context=null)
Handle an (deliberately caused) external error.
replaceVars($config)
Replace config keys with variables.
stripTraceObjects(&$trace, &$objects)
Separate objects from a trace.
errorHash($message, $filename, $line_no)
setAutoloadNamespaces($namespaces)
releaseNotes()
Add new release notes to messages and log.
setAutoloadFiles($classes)
errorHandler($error_no, $message, $filename, $line_no)
Error handler.
component($name)
Get a component.
halt($status=null)
End the request.
stripObjects(&$item, &$objects, $level=0)
Separate objects.
internalError($message, $filename=null, $line_no=null, $trace=null)
Handle an internal error.
autoload($class_name)
Autoloader.