Files
IPB/system/Data/Store.php
T
2025-12-19 19:17:57 -08:00

367 lines
8.5 KiB
PHP

<?php
/**
* @brief Abstract Storage Class
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 07 May 2013
*/
namespace IPS\Data;
/* To prevent PHP errors (extending class does not exist) revealing path */
use Exception;
use IPS\Data\Cache\None;
use IPS\Data\Store\Exception as StoreException;
use OutOfRangeException;
use function defined;
use function is_array;
use const IPS\CACHE_METHOD;
use const IPS\STORE_CONFIG;
use const IPS\STORE_METHOD;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Abstract Storage Class
*/
abstract class Store extends AbstractData
{
/**
* @brief Instance
*/
protected static ?Store $instance = NULL;
/**
* Available Data Store Methods
* - MUST always return 'Database' and 'FileSystem' options
*
* @return array<string, class-string<\IPS\Data\Store>>
*/
public static function availableMethods(): array
{
return [
'Database' => 'IPS\Data\Store\Database',
'FileSystem' => 'IPS\Data\Store\FileSystem',
'Redis' => 'IPS\Data\Store\Redis',
];
}
/**
* Get instance
*
* @return Store
*/
public static function i(): Store
{
if( static::$instance === NULL )
{
try
{
if( !isset( static::availableMethods()[ STORE_METHOD ] ) )
{
throw new StoreException;
}
$classname = static::availableMethods()[ STORE_METHOD ];
if( !$classname::supported() )
{
throw new StoreException;
}
static::$instance = new $classname( json_decode( STORE_CONFIG, TRUE ) );
}
catch( StoreException $e )
{
/* Fall back to MySQL (if not using it currently, and then FileSystem if we are) */
if ( STORE_METHOD !== 'Database' )
{
$classname = static::availableMethods()['Database'];
}
else
{
$classname = static::availableMethods()['FileSystem'];
}
static::$instance = new $classname( json_decode( STORE_CONFIG, TRUE ) );
}
}
return static::$instance;
}
/**
* @brief Always needed Store keys
*/
public array $initLoad = array();
/**
* @brief Template store keys
*/
public array $templateLoad = array();
/**
* @brief Log
*/
public array $log = array();
/**
* Test to see if this store works
*
* @return boolean
*/
public static function testStore(): bool
{
try
{
$classname = 'IPS\Data\Store\\' . STORE_METHOD;
$store = new $classname( json_decode( STORE_CONFIG, TRUE ) );
return $store->test();
}
catch( Exception $e )
{
return FALSE;
}
}
/**
* Load mutiple
* Used so if it is known that several are going to be needed, they can all be loaded into memory at the same time
*
* @param array $keys Keys
* @return void
*/
public function loadIntoMemory( array $keys )
{
}
/**
* Test the datastore engine to make sure it's working
*
* @return bool
*/
public function test(): bool
{
/* Set a test key */
$key = 'test_' . md5( mt_rand() );
$this->$key = '1';
/* Now clear our internal cache to ensure the value isn't returned from there */
unset( $this->_data[ $key ], $this->_exists[ $key ], static::$cache[ $key ] );
/* And then try to retrieve it */
if( isset( $this->$key ) )
{
$value = $this->$key;
unset( $this->$key );
return $value == 1;
}
return FALSE;
}
/**
* Magic Method: Get
*
* @param string $key Key
* @return mixed Value from the _datastore
*/
public function __get( string $key ): mixed
{
/* Try to get it from the cache store... */
try
{
/* If caching is enabled, and this isn't the special map of hashes... */
if ( $this->useCache( $key ) )
{
if ( Cache::i()->checkKeys() === false )
{
if ( isset( Cache::i()->$key ) and Cache::i()->$key !== NULL )
{
/* Cache engine always returns current content, so return it */
return Cache::i()->$key;
}
/* Still here? throw an exception so we get it from the data storage engine */
throw new OutOfRangeException;
}
else
{
/* It exists in the caching engine, and we know the hash of the correct value... */
$cacheKeys = ( isset( $this->cacheKeys ) and is_array( $this->cacheKeys ) ) ? $this->cacheKeys : array();
if ( array_key_exists( $key, $cacheKeys ) and isset( Cache::i()->$key ) )
{
/* Get it... */
$value = Cache::i()->$key;
/* But only use it if the hash matches */
if( $cacheKeys[ $key ] == md5( json_encode( $value ) ) )
{
return $value;
}
}
}
}
/* Still here? throw an exception so we get it from the data storage engine */
throw new OutOfRangeException;
}
/* If we couldn't get it from the cache engine, get it from the data storage engine */
catch ( OutOfRangeException $e )
{
/* Actually get it... */
$value = parent::__get( $key );
/* If caching is enabled, and this isn't the special map of hashes... */
if ( $this->useCache( $key ) )
{
/* Set it in the caching engine... */
Cache::i()->$key = $value;
if ( Cache::i()->checkKeys() === true )
{
/* And set the hash in the cacheKeys hash map */
$cacheKeys = ( isset( $this->cacheKeys ) and is_array( $this->cacheKeys ) ) ? $this->cacheKeys : array();
$cacheKeys[ $key ] = md5( json_encode( $value ) );
$this->cacheKeys = $cacheKeys;
}
}
return $value;
}
}
/**
* Magic Method: Set
*
* @param string $key Key
* @param mixed $value Value
* @return void
*/
public function __set( string $key, mixed $value )
{
/* Actually set it in the data storage engine */
parent::__set( $key, $value );
/* If caching is enabled, and this isn't the special map of hashes... */
if ( $this->useCache( $key ) )
{
/* Then also set it in the cache... */
Cache::i()->$key = $value;
/* And set the hash in the cacheKeys hash map */
if ( Cache::i()->checkKeys() === true )
{
if ( ( isset( $this->cacheKeys ) and is_array( $this->cacheKeys ) ) )
{
$cacheKeys = $this->cacheKeys;
$cacheKeys[ $key ] = md5( json_encode( $value ) );
$this->cacheKeys = $cacheKeys;
}
else
{
$this->cacheKeys = array( $key => md5( json_encode( $value ) ) );
}
}
}
}
/**
* Magic Method: Isset
*
* @param string $key Key
* @return bool
*/
public function __isset( string $key ): bool
{
/* If caching is enabled, and this isn't the special map of hashes, try to get it from the cache store... */
if ( $this->useCache( $key ) )
{
/* Does it exist in the caching engine? */
if ( isset( Cache::i()->$key ) )
{
/* Get it... */
$value = Cache::i()->$key;
if ( Cache::i()->checkKeys() === false )
{
/* Cache engine always returns current content, so return it */
return TRUE;
}
/* Only use it if the hash matches */
$cacheKeys = ( isset( $this->cacheKeys ) and is_array( $this->cacheKeys ) ) ? $this->cacheKeys : array();
if ( isset( $cacheKeys[ $key ] ) and $cacheKeys[ $key ] == md5( json_encode( $value ) ) )
{
return TRUE;
}
}
}
/* If we're still here, check the data storage engine... */
return parent::__isset( $key );
}
/**
* Magic Method: Unset
*
* @param string $key Key
* @return void
*/
public function __unset( string $key )
{
/* Unset it in the data storage engine */
parent::__unset( $key );
/* If caching is enabled, and this isn't the special map of hashes, remove it from the cache store... */
if ( $this->useCache( $key ) )
{
/* Remove it from the cache store... */
unset( Cache::i()->$key );
if ( Cache::i()->checkKeys() === true )
{
/* And from the special map of hashes */
try
{
$cacheKeys = is_array( $this->cacheKeys ) ? $this->cacheKeys : array();
}
/* If the cache doesn't exist we shouldn't error out with an uncaught OutOfRangeException */
catch( OutOfRangeException $e )
{
$cacheKeys = array();
}
if( isset( $cacheKeys[ $key ] ) )
{
unset( $cacheKeys[ $key ] );
}
$this->cacheKeys = $cacheKeys;
}
}
}
/**
* Check the cache for this key?
*
* @param string $key The store key
* @return boolean
*/
protected function useCache( string $key ): bool
{
return ( CACHE_METHOD != STORE_METHOD and ( ! Cache::i() instanceof None ) and $key !== 'cacheKeys' );
}
}