Version 4.5.0

This commit is contained in:
Neo committed 2025-12-19 05:51:58 -08:00
1 parent 1a0c7fd3c2
commit f79dcf067a
3791 files changed
+248032 -76372

No files matched your search

+242 -21
View File
@@ -11,7 +11,7 @@
namespace IPS\Patterns;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -54,9 +54,22 @@ abstract class _ActiveRecord
* @note This needs to be declared in any child classes as well, only declaring here for editor code-complete/error-check functionality
*/
protected static $multitons = array();
/**
* @brief [ActiveRecord] Caches
* @note Defined cache keys will be cleared automatically as needed
*/
protected $caches = array();
/**
* @brief [ActiveRecord] Attempt to load from cache
* @note If this is set to TRUE you should define a getStore() method to return the objects from cache
*/
protected static $loadFromCache = FALSE;
/**
* @brief [ActiveRecord] Database Connection
* @return \IPS\Db
*/
public static function db()
{
@@ -83,10 +96,25 @@ abstract class _ActiveRecord
}
/* If we did, check it's valid */
elseif( !in_array( $idField, static::$databaseIdFields ) )
elseif( !\in_array( $idField, static::$databaseIdFields ) )
{
throw new \InvalidArgumentException;
}
/* Some classes can load directly from a cache, so check that first */
if( static::$loadFromCache !== FALSE AND $idField === static::$databasePrefix . static::$databaseColumnId AND $extraWhereClause === NULL )
{
$cachedObjects = static::getStore();
if ( isset( $cachedObjects[ $id ] ) )
{
return static::constructFromData( $cachedObjects[ $id ] );
}
else
{
throw new \OutOfRangeException;
}
}
/* Does that exist in the multiton store? */
if ( !$extraWhereClause )
@@ -169,7 +197,7 @@ abstract class _ActiveRecord
$where = array( array( '`' . $idField . '`=?', $id ) );
if( $extraWhereClause !== NULL )
{
if ( !is_array( $extraWhereClause ) or !is_array( $extraWhereClause[0] ) )
if ( !\is_array( $extraWhereClause ) or !\is_array( $extraWhereClause[0] ) )
{
$extraWhereClause = array( $extraWhereClause );
}
@@ -208,7 +236,7 @@ abstract class _ActiveRecord
/* Initiate an object */
if ( !$obj )
{
$classname = get_called_class();
$classname = \get_called_class();
$obj = new $classname;
$obj->_new = FALSE;
$obj->_data = array();
@@ -251,7 +279,7 @@ abstract class _ActiveRecord
*/
public static function multitonIds()
{
if ( is_array( static::$multitons ) )
if ( \is_array( static::$multitons ) )
{
return array_keys( static::$multitons );
}
@@ -432,9 +460,9 @@ abstract class _ActiveRecord
{
foreach( $this->$k->values as $field => $value )
{
if ( isset( $data[ $field ] ) or $this->$k->originalValues[ $field ] != intval( $value ) )
if ( isset( $data[ $field ] ) or $this->$k->originalValues[ $field ] != \intval( $value ) )
{
$data[ $field ] = intval( $value );
$data[ $field ] = \intval( $value );
}
}
}
@@ -494,6 +522,8 @@ abstract class _ActiveRecord
/* Reset our log of what's changed */
$this->changed = array();
}
$this->clearCaches();
}
/**
@@ -516,26 +546,75 @@ abstract class _ActiveRecord
{
$idColumn = static::$databaseColumnId;
static::db()->delete( static::$databaseTable, array( static::$databasePrefix . $idColumn . '=?', $this->$idColumn ) );
$this->clearCaches( TRUE );
}
/**
* Get follow data
*
* @param string $area Area
* @param int $id ID
* @param int|array $id ID or array of IDs
* @param int $privacy static::FOLLOW_PUBLIC + static::FOLLOW_ANONYMOUS
* @param array $frequencyTypes array( 'immediate', 'daily', 'weekly' )
* @param array $frequencyTypes array( 'none', 'immediate', 'daily', 'weekly' )
* @param \IPS\DateTime|int|NULL $date Only users who started following before this date will be returned. NULL for no restriction
* @param int|array $limit LIMIT clause
* @param string $order Column to order by
* @param int $flags Flags to pass to select (e.g. \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS)
* @return \IPS\Db\Select
* @throws |\BadMethodCallException
* @param bool $countOnly Return only the count
* @return \Iterator|int
* @throws \BadMethodCallException
*/
protected static function _followers( $area, $id, $privacy, $frequencyTypes, $date, $limit, $order=NULL, $flags=\IPS\Db::SELECT_SQL_CALC_FOUND_ROWS )
protected static function _followers( $area, $id, $privacy, $frequencyTypes, $date=NULL, $limit=NULL, $order=NULL, $countOnly=FALSE )
{
/* Normalize the input */
sort( $frequencyTypes );
/* Can we use the cache table? */
$canCache = FALSE;
$cached = array();
if ( \count( $frequencyTypes ) == 4 and $countOnly and ( $privacy == static::FOLLOW_PUBLIC + static::FOLLOW_ANONYMOUS ) )
{
$canCache = TRUE;
if ( \is_array( $id ) )
{
foreach( \IPS\Db::i()->select( 'id, count', 'core_follow_count_cache', array( 'class=? and ' . \IPS\Db::i()->in( 'id', $id ), 'IPS\\' . static::$application . '\\' . ucfirst( $area ) ) ) as $row )
{
$cached[ $row['id'] ] = array( 'count' => $row['count'], 'follow_rel_id' => $row['id'] );
}
/* Got everything? */
if ( \count( $id ) == \count( $cached ) )
{
$obj = new \ArrayObject( $cached );
return $obj->getIterator();
}
}
else
{
try
{
return (int) \IPS\Db::i()->select( 'count', 'core_follow_count_cache', array('class=? and id=?', 'IPS\\' . static::$application . '\\' . ucfirst( $area ), $id) )->first();
}
catch ( \UnderflowException $e )
{
}
}
}
/* We need to use a group by if $id is an array, but otherwise not */
$groupBy = NULL;
/* Initial where clause */
$where[] = array( 'follow_app=? AND follow_area=? AND follow_rel_id=?', static::$application, $area, $id );
if( \is_array( $id ) )
{
$where[] = array( 'follow_app=? AND follow_area=? AND follow_rel_id IN(' . implode( ',', $id ) . ')', static::$application, $area );
$groupBy = 'follow_rel_id';
}
else
{
$where[] = array( 'follow_app=? AND follow_area=? AND follow_rel_id=?', static::$application, $area, $id );
}
/* Public / Anonymous */
if ( !( $privacy & static::FOLLOW_PUBLIC ) )
@@ -548,7 +627,7 @@ abstract class _ActiveRecord
}
/* Specific type */
if ( $frequencyTypes != array( 'immediate', 'daily', 'weekly' ) )
if ( \count( array_diff( array( 'immediate', 'daily', 'weekly', 'none' ), $frequencyTypes ) ) )
{
$where[] = array( \IPS\Db::i()->in( 'follow_notify_freq', $frequencyTypes ) );
}
@@ -556,30 +635,117 @@ abstract class _ActiveRecord
/* Since */
if( $date !== NULL )
{
$where[] = array( 'follow_added<?', ( $date instanceof \IPS\DateTime ) ? $date->getTimestamp() : intval( $date ) );
$where[] = array( 'follow_added<?', ( $date instanceof \IPS\DateTime ) ? $date->getTimestamp() : \intval( $date ) );
}
/* We don't need order or limit if we're doing a count only, which makes the query more efficient */
if( $countOnly === TRUE )
{
$limit = NULL;
$order = NULL;
}
/* Cache the results as this may be called multiple times in one page load */
static $cache = array();
$_hash = md5( json_encode( $where ) . $order . json_encode( $limit ) . $flags );
$_hash = md5( json_encode( \func_get_args() ) );
if( isset( $cache[ $_hash ] ) )
{
return $cache[ $_hash ];
}
/* Get */
if ( $order === 'name' )
{
$cache[ $_hash ] = \IPS\Db::i()->select( 'core_follow.*, core_members.name', 'core_follow', $where, 'name ASC', $limit, NULL, NULL, $flags )->join( 'core_members', array( 'core_members.member_id=core_follow.follow_member_id' ) );
$cache[ $_hash ] = \IPS\Db::i()->select( 'core_follow.*, core_members.name', 'core_follow', $where, 'name ASC', $limit )->join( 'core_members', array( 'core_members.member_id=core_follow.follow_member_id' ) );
}
else
{
$cache[ $_hash ] = \IPS\Db::i()->select( 'core_follow.*', 'core_follow', $where, $order, $limit, NULL, NULL, $flags );
$cache[ $_hash ] = \IPS\Db::i()->select( $countOnly ? ( \is_array( $id ) ? 'COUNT(*) as count, follow_rel_id' : 'COUNT(*)' ) : 'core_follow.*', 'core_follow', $where, $order, $limit, $groupBy );
}
/* If we only want the count, fetch it and store it now */
if( $countOnly )
{
if( \is_array( $id ) )
{
$args = \func_get_args();
foreach( $cache[ $_hash ] as $result )
{
$args[1] = $result['follow_rel_id'];
$cache[ md5( json_encode( $args ) ) ] = $result;
if ( $canCache and ! isset( $cached[ $result['follow_rel_id'] ] ) )
{
\IPS\Db::i()->replace( 'core_follow_count_cache', array(
'id' => $result['follow_rel_id'],
'class' => 'IPS\\' . static::$application . '\\' . ucfirst( $area ),
'count' => $result['count'],
'added' => time()
) );
}
$cached[ $result['follow_rel_id'] ] = $result;
}
/* And then any that do not exist were not found in the query, so they're 0 */
foreach( $id as $_id )
{
$args[1] = $_id;
$cache[ md5( json_encode( $args ) ) ] = array( 'follow_rel_id' => $id, 'count' => 0 );
if ( $canCache and ! isset( $cached[ $_id ] ) )
{
\IPS\Db::i()->replace( 'core_follow_count_cache', array(
'id' => $_id,
'class' => 'IPS\\' . static::$application . '\\' . ucfirst( $area ),
'count' => 0,
'added' => time()
) );
}
}
}
else
{
$cache[ $_hash ] = $cache[ $_hash ]->first();
if ( $canCache )
{
\IPS\Db::i()->replace( 'core_follow_count_cache', array(
'id' => $id,
'class' => 'IPS\\' . static::$application . '\\' . ucfirst( $area ),
'count' => $cache[ $_hash ],
'added' => time()
) );
}
}
}
if ( isset( $cached ) AND \is_array( $id ) )
{
$obj = new \ArrayObject( $cached );
return $obj->getIterator();
}
return $cache[ $_hash ];
}
/**
* Get follower count
*
* @param string $area Area
* @param int|array $id ID or array of IDs
* @param int $privacy static::FOLLOW_PUBLIC + static::FOLLOW_ANONYMOUS
* @param array $frequencyTypes array( 'immediate', 'daily', 'weekly' )
* @param \IPS\DateTime|int|NULL $date Only users who started following before this date will be returned. NULL for no restriction
* @return int
* @throws |\BadMethodCallException
*/
protected static function _followersCount( $area, $id, $privacy=3, $frequencyTypes=array( 'immediate', 'daily', 'weekly', 'none' ), $date=NULL )
{
return static::_followers( $area, $id, $privacy, $frequencyTypes, $date, NULL, NULL, TRUE );
}
/**
* Cover Photo
@@ -625,10 +791,65 @@ abstract class _ActiveRecord
for( $i=0, $j=\strlen($string); $i<$j; $i++ )
{
$integer = ord( \substr( $string, $i, 1 ) ) + ( ( $integer << 5 ) - $integer );
$integer = \ord( \substr( $string, $i, 1 ) ) + ( ( $integer << 5 ) - $integer );
$integer = $integer & $integer;
}
return "hsl(" . ( $integer % 360 ) . ", 100%, 80% )";
}
/**
* Clear any defined caches
*
* @param bool $removeMultiton Should the multiton record also be removed?
* @return void
*/
public function clearCaches( $removeMultiton=FALSE )
{
if( \count( $this->caches ) )
{
foreach( $this->caches as $cacheKey )
{
unset( \IPS\Data\Store::i()->$cacheKey );
}
}
if( $removeMultiton === TRUE )
{
$idColumn = static::$databaseColumnId;
if ( isset( static::$multitons[ $this->$idColumn ] ) )
{
unset( static::$multitons[ $this->$idColumn ] );
if ( isset( static::$multitonMap ) )
{
foreach ( static::$databaseIdFields as $field )
{
if( isset( static::$multitonMap[ $field ] ) )
{
foreach( static::$multitonMap[ $field ] as $otherId => $mappedId )
{
if( $mappedId == $this->$idColumn )
{
unset( static::$multitonMap[ $field ][ $otherId ] );
}
}
}
}
}
}
}
}
/**
* Attempt to load cached data
*
* @note This should be overridden in your class if you define $cacheToLoadFrom
* @return mixed
*/
public static function getStore()
{
return iterator_to_array( \IPS\Db::i()->select( '*', static::$databaseTable, NULL, static::$databasePrefix . static::$databaseColumnId )->setKeyField( static::$databasePrefix . static::$databaseColumnId ) );
}
}
+33 -9
View File
@@ -11,7 +11,7 @@
namespace IPS\Patterns;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -26,17 +26,25 @@ class _ActiveRecordIterator extends \IteratorIterator implements \Countable
* @brief Classname
*/
public $classname;
/**
* @brief Parent classname
*/
public $parentClassname = NULL;
/**
* Constructor
*
* @param \Traversable $iterator The iterator
* @param string $classname The classname
* @param \Traversable $iterator The iterator
* @param string $classname The classname
* @param string|NULL $parentClass Parent classname, if the parent data is also available (e.g. joined in a query)
* @return void
*/
public function __construct( \Traversable $iterator, $classname )
public function __construct( \Traversable $iterator, $classname, $parentClassname = NULL )
{
$this->classname = $classname;
$this->classname = $classname;
$this->parentClassname = $parentClassname;
return parent::__construct( $iterator );
}
@@ -47,18 +55,34 @@ class _ActiveRecordIterator extends \IteratorIterator implements \Countable
*/
public function current()
{
$current = parent::current();
/* This is here purely to prime caches to prevent queries later. If we've already joined (or otherwise have) the data for another class, then we can construct
an active record object of it in memory to prevent calls to load() against that class resulting in separate queries later. */
if( $this->parentClassname !== NULL )
{
$parentClass = $this->parentClassname;
/* We need to make sure we actually have the data first though */
$idColumn = $parentClass::$databasePrefix . $parentClass::$databaseColumnId;
if( isset( $current[ $idColumn ] ) )
{
$parentClass::constructFromData( $current );
}
}
$classname = $this->classname;
return $classname::constructFromData( parent::current() );
return $classname::constructFromData( $current );
}
/**
* Get count
*
* @param bool $allRows If TRUE, will get the number of rows ignoring the limit. In order for this to work, the query must have been ran with SQL_CALC_FOUND_ROWS
* @return int
*/
public function count( $allRows = FALSE )
public function count()
{
return (int) $this->getInnerIterator()->count( $allRows );
return (int) $this->getInnerIterator()->count();
}
}
+4 -3
View File
@@ -11,7 +11,7 @@
namespace IPS\Patterns;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -65,7 +65,7 @@ class _Bitwise implements \ArrayAccess
$i = 1;
foreach ( $fields as $k => $v )
{
if ( is_string( $v ) )
if ( \is_string( $v ) )
{
$this->keys[ $groupKey ][ $v ] = $i;
$this->lookup[ $k ] = $groupKey;
@@ -124,7 +124,8 @@ class _Bitwise implements \ArrayAccess
{
if ( $this->callback !== NULL )
{
call_user_func( $this->callback, $offset, $value );
$callback = $this->callback;
$callback( $offset, $value );
}
$group = $this->lookup[ $offset ];
+2 -2
View File
@@ -11,7 +11,7 @@
namespace IPS\Patterns;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -37,7 +37,7 @@ class _Singleton implements \Iterator
{
if( static::$instance === NULL )
{
$classname = get_called_class();
$classname = \get_called_class();
static::$instance = new $classname;
}
+3 -3
View File
@@ -11,7 +11,7 @@
namespace IPS\Patterns;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -121,7 +121,7 @@ class _UnionIterator implements \Iterator, \Countable
{
$col = $this->iterators->getInfo();
$current = $iterator->current();
if ( is_array( $current ) )
if ( \is_array( $current ) )
{
$value = $current[ $col ];
}
@@ -164,7 +164,7 @@ class _UnionIterator implements \Iterator, \Countable
$count = 0;
foreach ( $this->iterators as $iterator )
{
$count += count( $iterator );
$count += \count( $iterator );
}
return $count;
}