Add caching to the auto_loader for interfaces (#7320)

* update auto loader to load and cache interfaces

* use new method to update the auto loader cache

* use new interface

* use new interface to trigger a cache flush

* update the console upgrade menu

* update the missing implements syntax

* add the clear_cache to the flush cache button
This commit is contained in:
frytimo 2025-03-15 12:13:36 -03:00 committed by GitHub
parent 99b3100ee9
commit 0a42d8f198
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 251 additions and 98 deletions

View File

@ -101,6 +101,10 @@
case "cache-flush": case "cache-flush":
$cache = new cache; $cache = new cache;
$response = $cache->flush(); $response = $cache->flush();
//trigger clear cache for any classes that require it
foreach ($autoload->get_interface_list('clear_cache') as $class) {
$class::clear_cache();
}
message::add($response, 'alert'); message::add($response, 'alert');
break; break;
case "reloadxml": case "reloadxml":

View File

@ -621,6 +621,33 @@ $text['label-update_filesystem_permissions']['zh-cn'] = "";
$text['label-update_filesystem_permissions']['ja-jp'] = ""; $text['label-update_filesystem_permissions']['ja-jp'] = "";
$text['label-update_filesystem_permissions']['ko-kr'] = ""; $text['label-update_filesystem_permissions']['ko-kr'] = "";
$text['message-updated_autoloader']['en-us'] = 'Updated autoloader';
$text['message-updated_autoloader']['en-gb'] = 'Updated autoloader';
$text['message-updated_autoloader']['ar-eg'] = 'تم تحديث أداة التحميل التلقائي';
$text['message-updated_autoloader']['de-at'] = 'Aktualisierter Autoloader';
$text['message-updated_autoloader']['de-ch'] = 'Aktualisierter Autoloader';
$text['message-updated_autoloader']['de-de'] = 'Aktualisierter Autoloader';
$text['message-updated_autoloader']['el-gr'] = 'Ενημερώθηκε η αυτόματη φόρτωση';
$text['message-updated_autoloader']['es-cl'] = 'Cargador automático actualizado';
$text['message-updated_autoloader']['es-mx'] = 'Cargador automático actualizado';
$text['message-updated_autoloader']['fr-ca'] = 'Chargeur automatique mis à jour';
$text['message-updated_autoloader']['fr-fr'] = 'Chargeur automatique mis à jour';
$text['message-updated_autoloader']['he-il'] = 'טוען אוטומטי מעודכן';
$text['message-updated_autoloader']['it-it'] = 'Caricatore automatico aggiornato';
$text['message-updated_autoloader']['ka-ge'] = 'განახლებულია ავტოჩამტვირთავი';
$text['message-updated_autoloader']['nl-nl'] = 'Bijgewerkte autoloader';
$text['message-updated_autoloader']['pl-pl'] = 'Zaktualizowany autoloader';
$text['message-updated_autoloader']['pt-br'] = 'Carregador automático atualizado';
$text['message-updated_autoloader']['pt-pt'] = 'Carregador automático atualizado';
$text['message-updated_autoloader']['ro-ro'] = 'Încărcare automată actualizată';
$text['message-updated_autoloader']['ru-ru'] = 'Обновленный автозагрузчик';
$text['message-updated_autoloader']['sv-se'] = 'Uppdaterad autoloader';
$text['message-updated_autoloader']['uk-ua'] = 'Оновлений автозавантажувач';
$text['message-updated_autoloader']['tr-tr'] = 'Güncellenmiş otomatik yükleyici';
$text['message-updated_autoloader']['zh-cn'] = '更新的自动加载器';
$text['message-updated_autoloader']['ja-jp'] = 'オートローダーの更新';
$text['message-updated_autoloader']['ko-kr'] = '업데이트된 자동 로더';
$text['label-upgrade_menu']['en-us'] = "Menu Defaults"; $text['label-upgrade_menu']['en-us'] = "Menu Defaults";
$text['label-upgrade_menu']['en-gb'] = "Menu Defaults"; $text['label-upgrade_menu']['en-gb'] = "Menu Defaults";
$text['label-upgrade_menu']['ar-eg'] = "افتراضيات القائمة"; $text['label-upgrade_menu']['ar-eg'] = "افتراضيات القائمة";

View File

@ -141,9 +141,6 @@
fclose($file_handle); fclose($file_handle);
} }
//include files
require dirname(__DIR__, 2) . "/resources/require.php";
//check the permission //check the permission
if(defined('STDIN')) { if(defined('STDIN')) {
$display_type = 'text'; //html, text $display_type = 'text'; //html, text
@ -177,11 +174,13 @@
exit(); exit();
} }
//always update the auto_loader cache just-in-case the source files have updated //always update the now global autoload cache just-in-case the source files have updated
$auto_loader = new auto_loader(); $autoload->update();
$auto_loader->reload_classes();
$auto_loader->update_cache(); //trigger clear cache for any classes that require it
$auto_loader->clear_cache(); foreach ($autoload->get_interface_list('clear_cache') as $class) {
$class::clear_cache();
}
//get the version of the software //get the version of the software
if ($upgrade_type == 'version') { if ($upgrade_type == 'version') {

View File

@ -147,18 +147,13 @@ function show_upgrade_menu() {
* @global type $text * @global type $text
*/ */
function do_upgrade_auto_loader() { function do_upgrade_auto_loader() {
global $text; global $text, $autoload;
//remove temp file //remove temp files
unlink(sys_get_temp_dir() . '/' . auto_loader::FILE); unlink(sys_get_temp_dir() . '/' . auto_loader::CLASSES_FILE);
unlink(sys_get_temp_dir() . '/' . auto_loader::INTERFACES_FILE);
//create a new instance of the autoloader //create a new instance of the autoloader
$loader = new auto_loader(); $autoload->update();
//reload the classes echo "{$text['message-updated_autoloader']}\n";
$loader->reload_classes();
echo "{$text['label-reloaded_classes']}\n";
//re-create cache file
if ($loader->update_cache()) {
echo "{$text['label-updated_cache']}\n";
}
} }
/** /**
@ -195,7 +190,7 @@ function do_filesystem_permissions($text, settings $settings) {
$directories[] = $log_directory . '/xml_cdr'; $directories[] = $log_directory . '/xml_cdr';
} }
//update the auto_loader cache permissions file //update the auto_loader cache permissions file
$directories[] = sys_get_temp_dir() . '/' . auto_loader::FILE; $directories[] = sys_get_temp_dir() . '/' . auto_loader::CLASSES_FILE;
//execute chown command for each directory //execute chown command for each directory
foreach ($directories as $dir) { foreach ($directories as $dir) {
if ($dir !== null) { if ($dir !== null) {

View File

@ -34,8 +34,10 @@
*/ */
class auto_loader { class auto_loader {
const FILE = 'autoloader_cache.php'; const CLASSES_KEY = 'autoloader_classes';
const CACHE_KEY = 'autoloader_classes'; const CLASSES_FILE = 'autoloader_cache.php';
const INTERFACES_KEY = "autoloader_interfaces";
const INTERFACES_FILE = "autoloader_interface_cache.php";
private $classes; private $classes;
@ -46,23 +48,36 @@ class auto_loader {
private $apcu_enabled; private $apcu_enabled;
/** /**
* Cache path and file name * Cache path and file name for classes
* @var string * @var string
*/ */
private static $cache_file = null; private static $classes_file = null;
public function __construct($project_path = '') { /**
* Maps interfaces to classes
* @var array
*/
private $interfaces;
/**
* Cache path and file name for interfaces
* @var string
*/
private static $interfaces_file = null;
public function __construct() {
//set if we can use RAM cache //set if we can use RAM cache
$this->apcu_enabled = function_exists('apcu_enabled') && apcu_enabled(); $this->apcu_enabled = function_exists('apcu_enabled') && apcu_enabled();
//set cache location //set cache location
self::$cache_file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . self::FILE; self::$classes_file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . self::CLASSES_FILE;
self::$interfaces_file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . self::INTERFACES_FILE;
//classes must be loaded before this object is registered //classes must be loaded before this object is registered
if (!$this->load_cache()) { if (!$this->load_cache()) {
//cache miss so load them //cache miss so load them
$this->reload_classes($project_path); $this->reload_classes();
//update the cache after loading classes array //update the cache after loading classes array
$this->update_cache(); $this->update_cache();
} }
@ -70,7 +85,16 @@ class auto_loader {
spl_autoload_register(array($this, 'loader')); spl_autoload_register(array($this, 'loader'));
} }
public function update_cache(string $file = ''): bool { /**
* Update the auto loader
*/
public function update() {
$this->clear_cache();
$this->reload_classes();
$this->update_cache();
}
public function update_cache(): bool {
//guard against writing an empty file //guard against writing an empty file
if (empty($this->classes)) { if (empty($this->classes)) {
return false; return false;
@ -78,65 +102,74 @@ class auto_loader {
//update RAM cache when available //update RAM cache when available
if ($this->apcu_enabled) { if ($this->apcu_enabled) {
$success = apcu_store(self::CACHE_KEY, $this->classes); $classes_cached = apcu_store(self::CLASSES_KEY, $this->classes);
$interfaces_cached = apcu_store(self::INTERFACES_KEY, $this->interfaces);
//do not save to drive when we are using apcu //do not save to drive when we are using apcu
if ($success) return true; if ($classes_cached && $interfaces_cached) return true;
}
//ensure we have somewhere to put the file
if (empty($file)) {
$file = self::$cache_file;
} }
//export the classes array using PHP engine //export the classes array using PHP engine
$data = var_export($this->classes, true); $classes_array = var_export($this->classes, true);
//put the array in a form that it can be loaded directly to an array //put the array in a form that it can be loaded directly to an array
$result = file_put_contents($file, "<?php\n return " . $data . ";\n"); $class_result = file_put_contents(self::$classes_file, "<?php\n return " . $classes_array . ";\n");
if ($result !== false) { if ($class_result === false) {
return true; //file failed to save - send error to syslog when debugging
$error_array = error_get_last();
self::log(LOG_WARNING, $error_array['message'] ?? '');
} }
//file failed to save - send error to syslog when debugging //export the interfaces array using PHP engine
$error_array = error_get_last(); $interfaces_array = var_export($this->interfaces, true);
self::log(LOG_WARNING, $error_array['message'] ?? '');
return false; //put the array in a form that it can be loaded directly to an array
$interface_result = file_put_contents(self::$interfaces_file, "<?php\n return " . $interfaces_array . ";\n");
if ($interface_result === false) {
//file failed to save - send error to syslog when debugging
$error_array = error_get_last();
self::log(LOG_WARNING, $error_array['message'] ?? '');
}
$result = ($class_result && $interface_result);
return $result;
} }
public function load_cache(string $file = ''): bool { public function load_cache(): bool {
$this->classes = []; $this->classes = [];
$this->interfaces = [];
//use apcu when available //use apcu when available
if ($this->apcu_enabled && apcu_exists(self::CACHE_KEY)) { if ($this->apcu_enabled && apcu_exists(self::CLASSES_KEY)) {
$this->classes = apcu_fetch(self::CACHE_KEY, $success); $this->classes = apcu_fetch(self::CLASSES_KEY, $classes_cached);
if ($success) return true; $this->interfaces = apcu_fetch(self::INTERFACES_KEY, $interfaces_cached);
} //don't use files when we are using apcu caching
if ($classes_cached && $interfaces_cached) return true;
//use a standard file
if (empty($file)) {
$file = self::$cache_file;
} }
//use PHP engine to parse it //use PHP engine to parse it
if (file_exists($file)) { if (file_exists(self::$classes_file)) {
$this->classes = include $file; $this->classes = include self::$classes_file;
}
//do the same for interface to class mappings
if (file_exists(self::$interfaces_file)) {
$this->interfaces = include self::$interfaces_file;
} }
//catch edge case of first time using apcu cache //catch edge case of first time using apcu cache
if ($this->apcu_enabled) { if ($this->apcu_enabled) {
apcu_store(self::CACHE_KEY, $this->classes); apcu_store(self::CLASSES_KEY, $this->classes);
apcu_store(self::INTERFACES_KEY, $this->interfaces);
} }
//return true when we have classes and false if the array is still empty //return true when we have classes and false if the array is still empty
return !empty($this->classes); return (!empty($this->classes) && !empty($this->interfaces));
} }
public function reload_classes($project_path = '') { public function reload_classes() {
//set project path using magic dir constant //set project path using magic dir constant
if (empty($project_path)) { $project_path = dirname(__DIR__, 2);
$project_path = dirname(__DIR__, 2);
}
//build the array of all locations for classes in specific order //build the array of all locations for classes in specific order
$search_path = [ $search_path = [
@ -156,7 +189,7 @@ class auto_loader {
} }
//reset the current array //reset the current array
$this->classes = []; $class_list = [];
//store PHP language declared classes, interfaces, and traits //store PHP language declared classes, interfaces, and traits
$current_classes = get_declared_classes(); $current_classes = get_declared_classes();
@ -184,7 +217,7 @@ class auto_loader {
$classes = array_diff($new_classes, $current_classes); $classes = array_diff($new_classes, $current_classes);
if (!empty($classes)) { if (!empty($classes)) {
foreach ($classes as $class) { foreach ($classes as $class) {
$this->classes[$class] = $file; $class_list[$class] = $file;
} }
//overwrite previous array with new values //overwrite previous array with new values
$current_classes = $new_classes; $current_classes = $new_classes;
@ -194,7 +227,7 @@ class auto_loader {
$interfaces = array_diff($new_interfaces, $current_interfaces); $interfaces = array_diff($new_interfaces, $current_interfaces);
if (!empty($interfaces)) { if (!empty($interfaces)) {
foreach ($interfaces as $interface) { foreach ($interfaces as $interface) {
$this->classes[$interface] = $file; $class_list[$interface] = $file;
} }
//overwrite previous array with new values //overwrite previous array with new values
$current_interfaces = $new_interfaces; $current_interfaces = $new_interfaces;
@ -204,12 +237,24 @@ class auto_loader {
$traits = array_diff($new_traits, $current_traits); $traits = array_diff($new_traits, $current_traits);
if (!empty($traits)) { if (!empty($traits)) {
foreach ($traits as $trait) { foreach ($traits as $trait) {
$this->classes[$trait] = $file; $class_list[$trait] = $file;
} }
//overwrite previous array with new values //overwrite previous array with new values
$current_traits = $new_traits; $current_traits = $new_traits;
} }
} }
//add only new classes
if (!empty($class_list)) {
foreach ($class_list as $class => $location) {
if (!isset($this->classes[$class])) {
$this->classes[$class] = $location;
}
}
}
//load interface to class mappings
$this->reload_interface_list();
} }
/** /**
@ -276,6 +321,24 @@ class auto_loader {
return false; return false;
} }
public function reload_interface_list() {
$this->interfaces = [];
if (!empty($this->classes)) {
$classes = array_keys($this->classes);
foreach ($classes as $class) {
$reflection = new ReflectionClass($class);
$interfaces = $reflection->getInterfaces();
foreach ($interfaces as $interface) {
$name = $interface->getName();
if (empty($this->interfaces[$name])) {
$this->interfaces[$name] = [];
}
$this->interfaces[$name][] = $class;
}
}
}
}
/** /**
* Returns a list of classes loaded by the auto_loader. If no classes have been loaded an empty array is returned. * Returns a list of classes loaded by the auto_loader. If no classes have been loaded an empty array is returned.
* @return array List of classes loaded by the auto_loader or empty array * @return array List of classes loaded by the auto_loader or empty array
@ -287,26 +350,70 @@ class auto_loader {
return []; return [];
} }
public static function clear_cache(string $file = '') { /**
* Returns a list of classes implementing the interface
* @param string $interface_name
* @return array
*/
public function get_interface_list(string $interface_name): array {
//make sure we can return values
if (empty($this->classes) || empty($this->interfaces)) {
return [];
}
//check if we have an interface with that name
if (!empty($this->interfaces[$interface_name])) {
//return the list of classes associated with that interface
return $this->interfaces[$interface_name];
}
//interface is not implemented by any classes
return [];
}
public function get_interfaces(): array {
if (!empty($this->interfaces)) {
return $this->interfaces;
}
return [];
}
public static function clear_cache(string $classes_file = '', string $interfaces_file = '') {
//check for apcu cache //check for apcu cache
if (function_exists('apcu_enabled') && apcu_enabled()) { if (function_exists('apcu_enabled') && apcu_enabled()) {
apcu_delete(self::CACHE_KEY); apcu_delete(self::CLASSES_KEY);
apcu_delete(self::INTERFACES_KEY);
} }
//set default file //set default file
if (empty(self::$cache_file)) { if (empty(self::$classes_file)) {
self::$cache_file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . self::FILE; self::$classes_file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . self::CLASSES_FILE;
} }
//set file to clear //set file to clear
if (empty($file)) { if (empty($classes_file)) {
$file = self::$cache_file; $classes_file = self::$classes_file;
} }
//remove the file when it exists //remove the file when it exists
if (file_exists($file)) { if (file_exists($classes_file)) {
@unlink($file); @unlink($classes_file);
$error_array = error_get_last();
//send to syslog when debugging with either environment variable or debug in the url
self::log(LOG_WARNING, $error_array['message'] ?? '');
}
if (empty(self::$interfaces_file)) {
self::$interfaces_file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . self::INTERFACES_FILE;
}
//set interfaces file to clear
if (empty($interfaces_file)) {
$interfaces_file = self::$interfaces_file;
}
//remove the file when it exists
if (file_exists($interfaces_file)) {
@unlink($interfaces_file);
$error_array = error_get_last(); $error_array = error_get_last();
//send to syslog when debugging with either environment variable or debug in the url //send to syslog when debugging with either environment variable or debug in the url
self::log(LOG_WARNING, $error_array['message'] ?? ''); self::log(LOG_WARNING, $error_array['message'] ?? '');

View File

@ -9,7 +9,7 @@
* @access public * @access public
* @author Mark Crane <mark@fusionpbx.com> * @author Mark Crane <mark@fusionpbx.com>
*/ */
class settings { class settings implements clear_cache {
/** /**
* Set in the constructor. String used to load a specific domain. Must be a value domain UUID before sending to the constructor. * Set in the constructor. String used to load a specific domain. Must be a value domain UUID before sending to the constructor.
@ -446,31 +446,13 @@ class settings {
} }
} }
/** public static function clear_cache() {
* Clears the settings cache $cache = apcu_cache_info(false);
* @param string $type Empty clears all settings. Values can be global, domain, or user. if (!empty($cache['cache_list'])) {
*/ foreach ($cache['cache_list'] as $entry) {
public static function clear_cache(string $type = '') { $key = $entry['info'];
if (function_exists('apcu_enabled') && apcu_enabled()) { if (str_starts_with($key, 'settings_')) {
switch ($type) { apcu_delete($key);
case 'all':
case '':
default:
$type = "";
break;
case 'global':
case 'domain':
case 'user':
$type .= "_";
break;
}
$cache = apcu_cache_info(false);
if (!empty($cache['cache_list'])) {
foreach ($cache['cache_list'] as $entry) {
$key = $entry['info'];
if (str_starts_with($key, 'settings_' . $type)) {
apcu_delete($key);
}
} }
} }
} }

View File

@ -0,0 +1,39 @@
<?php
/*
* FusionPBX
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is FusionPBX
*
* The Initial Developer of the Original Code is
* Mark J Crane <markjcrane@fusionpbx.com>
* Portions created by the Initial Developer are Copyright (C) 2008-2025
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Mark J Crane <markjcrane@fusionpbx.com>
* Tim Fry <tim@fusionpbx.com>
*/
/**
* Any class that implements the clear_cache interface will automatically be called when the cache button is pressed
* @author Tim Fry <tim@fusionpbx.com>
*/
interface clear_cache {
/**
* The clear_cache method is called automatically for any class that implements the clear_cache interface.
* The function declared here ensures that all clear_cache methods have the same number of parameters being passed, which in this case, is no parameters.
*/
public static function clear_cache();
}