Seditio Source
Root |
./othercms/ips_4.3.4/system/Patterns/ActiveRecord.php
<?php
/**
 * @brief        Active Record Pattern
 * @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        18 Feb 2013
 */

namespace IPS\Patterns;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
   
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
    exit;
}

/**
 * Active Record Pattern
 */
abstract class _ActiveRecord
{
   
/**
     * @brief    [ActiveRecord] Database Prefix
     */
   
public static $databasePrefix = '';
   
   
/**
     * @brief    [ActiveRecord] ID Database Column
     */
   
public static $databaseColumnId = 'id';

   
/**
     * @brief    [ActiveRecord] Database table
     * @note    This MUST be over-ridden
     */
   
public static $databaseTable    = '';
       
   
/**
     * @brief    [ActiveRecord] Database ID Fields
     * @note    If using this, declare a static $multitonMap = array(); in the child class to prevent duplicate loading queries
     */
   
protected static $databaseIdFields = array();
   
   
/**
     * @brief    Bitwise keys
     */
   
protected static $bitOptions = array();

   
/**
     * @brief    [ActiveRecord] Multiton Store
     * @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] Database Connection
     */
   
public static function db()
    {
        return \
IPS\Db::i();
    }
   
   
/**
     * Load Record
     *
     * @see        \IPS\Db::build
     * @param    int|string    $id                    ID
     * @param    string        $idField            The database column that the $id parameter pertains to (NULL will use static::$databaseColumnId)
     * @param    mixed        $extraWhereClause    Additional where clause(s) (see \IPS\Db::build for details) - if used will cause multiton store to be skipped and a query always ran
     * @return    static
     * @throws    \InvalidArgumentException
     * @throws    \OutOfRangeException
     */
   
public static function load( $id, $idField=NULL, $extraWhereClause=NULL )
    {
       
/* If we didn't specify an ID field, assume the default */
       
if( $idField === NULL )
        {
           
$idField = static::$databasePrefix . static::$databaseColumnId;
        }
       
       
/* If we did, check it's valid */
       
elseif( !in_array( $idField, static::$databaseIdFields ) )
        {
            throw new \
InvalidArgumentException;
        }
               
       
/* Does that exist in the multiton store? */
       
if ( !$extraWhereClause )
        {
            if(
$idField === static::$databasePrefix . static::$databaseColumnId )
            {
                if ( !empty( static::
$multitons[ $id ] ) )
                {
                    return static::
$multitons[ $id ];
                }
            }
            elseif ( isset( static::
$multitonMap ) and isset( static::$multitonMap[ $idField ][ $id ] ) )
            {
                return static::
$multitons[ static::$multitonMap[ $idField ][ $id ] ];
            }
        }
       
       
/* Load it */
       
try
        {
           
$row = static::constructLoadQuery( $id, $idField, $extraWhereClause )->first();
        }
        catch ( \
UnderflowException $e )
        {
            throw new \
OutOfRangeException;
        }
       
       
/* If it doesn't exist in the multiton store, set it */
       
if( !isset( static::$multitons[ $row[ static::$databasePrefix . static::$databaseColumnId ] ] ) )
        {
            static::
$multitons[ $row[ static::$databasePrefix . static::$databaseColumnId ] ] = static::constructFromData( $row );
        }
        if ( isset( static::
$multitonMap ) )
        {
            foreach ( static::
$databaseIdFields as $field )
            {
                if (
$row[ $field ] )
                {
                    static::
$multitonMap[ $field ][ $row[ $field ] ] = $row[ static::$databasePrefix . static::$databaseColumnId ];
                }
            }
        }
       
       
/* And return it */
       
return static::$multitons[ $row[ static::$databasePrefix . static::$databaseColumnId ] ];
    }

   
/**
     * Load record based on a URL
     *
     * @param    \IPS\Http\Url    $url    URL to load from
     * @return    static
     * @throws    \InvalidArgumentException
     * @throws    \OutOfRangeException
     */
   
public static function loadFromUrl( \IPS\Http\Url $url )
    {        
        if ( isset(
$url->queryString['id'] ) )
        {
            return static::
load( $url->queryString['id'] );
        }
        if ( isset(
$url->hiddenQueryString['id'] ) )
        {
            return static::
load( $url->hiddenQueryString['id'] );
        }
       
        throw new \
InvalidArgumentException;
    }

   
/**
     * Construct Load Query
     *
     * @param    int|string    $id                    ID
     * @param    string        $idField            The database column that the $id parameter pertains to
     * @param    mixed        $extraWhereClause    Additional where clause(s)
     * @return    \IPS\Db\Select
     */
   
protected static function constructLoadQuery( $id, $idField, $extraWhereClause )
    {
       
$where = array( array( '`' . $idField . '`=?', $id ) );
        if(
$extraWhereClause !== NULL )
        {
            if ( !
is_array( $extraWhereClause ) or !is_array( $extraWhereClause[0] ) )
            {
               
$extraWhereClause = array( $extraWhereClause );
            }
           
$where = array_merge( $where, $extraWhereClause );
        }
       
        return static::
db()->select( '*', static::$databaseTable, $where );
    }
           
   
/**
     * Construct ActiveRecord from database row
     *
     * @param    array    $data                            Row from database table
     * @param    bool    $updateMultitonStoreIfExists    Replace current object in multiton store if it already exists there?
     * @return    static
     */
   
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
    {
       
/* Does that exist in the multiton store? */
       
$obj = NULL;
        if ( isset( static::
$databaseColumnId ) )
        {
           
$idField = static::$databasePrefix . static::$databaseColumnId;
           
$id = $data[ $idField ];
           
            if( isset( static::
$multitons[ $id ] ) )
            {
                if ( !
$updateMultitonStoreIfExists )
                {
                    return static::
$multitons[ $id ];
                }
               
$obj = static::$multitons[ $id ];
            }
        }
       
       
/* Initiate an object */
       
if ( !$obj )
        {
           
$classname = get_called_class();
           
$obj = new $classname;
           
$obj->_new  = FALSE;
           
$obj->_data = array();
        }
       
       
/* Import data */
       
$databasePrefixLength = \strlen( static::$databasePrefix );
        foreach (
$data as $k => $v )
        {
            if( static::
$databasePrefix AND mb_strpos( $k, static::$databasePrefix ) === 0 )
            {
               
$k = \substr( $k, $databasePrefixLength );
            }

           
$obj->_data[ $k ] = $v;
        }
       
       
$obj->changed = array();
       
       
/* Init */
       
if ( method_exists( $obj, 'init' ) )
        {
           
$obj->init();
        }
       
       
/* If it doesn't exist in the multiton store, set it */
       
if( isset( static::$databaseColumnId ) and !isset( static::$multitons[ $id ] ) )
        {
            static::
$multitons[ $id ] = $obj;
        }
               
       
/* Return */
       
return $obj;
    }
   
   
/**
     * Get which IDs are already loaded
     *
     * @return    array
     */
   
public static function multitonIds()
    {
        if (
is_array( static::$multitons ) )
        {
            return
array_keys( static::$multitons );
        }
        return array();
    }
   
   
/**
     * @brief    Data Store
     */
   
protected $_data = array();
   
   
/**
     * @brief    Is new record?
     */
   
protected $_new = TRUE;
       
   
/**
     * @brief    Changed Columns
     */
   
public $changed = array();
   
   
/**
     * Constructor - Create a blank object with default values
     *
     * @return    void
     */
   
public function __construct()
    {                        
       
$this->setDefaultValues();
    }
   
   
/**
     * Set Default Values (overriding $defaultValues)
     *
     * @return    void
     */
   
protected function setDefaultValues()
    {
       
    }
       
   
/**
     * Get value from data store
     *
     * @param    mixed    $key    Key
     * @return    mixed    Value from the datastore
     */
   
public function __get( $key )
    {
        if(
method_exists( $this, 'get_'.$key ) )
        {
           
$method = 'get_' . $key;
            return
$this->$method();
        }
        elseif( isset(
$this->_data[ $key ] ) or isset( static::$bitOptions[ $key ] ) )
        {
            if ( isset( static::
$bitOptions[ $key ] ) )
            {
                if ( !isset(
$this->_data[ $key ] ) or !( $this->_data[ $key ] instanceof Bitwise ) )
                {
                   
$values = array();
                    foreach ( static::
$bitOptions[ $key ] as $k => $map )
                    {
                       
$values[ $k ] = isset( $this->_data[ $k ] ) ? $this->_data[ $k ] : 0;
                    }
                   
$this->_data[ $key ] = new Bitwise( $values, static::$bitOptions[ $key ], method_exists( $this, "setBitwise_{$key}" ) ? array( $this, "setBitwise_{$key}" ) : NULL );
                }
            }
            return
$this->_data[ $key ];
        }
               
        return
NULL;
    }
   
   
/**
     * Set value in data store
     *
     * @see        \IPS\Patterns\ActiveRecord::save
     * @param    mixed    $key    Key
     * @param    mixed    $value    Value
     * @return    void
     */
   
public function __set( $key, $value )
    {
        if(
method_exists( $this, 'set_'.$key ) )
        {
           
$oldValues = $this->_data;
           
           
$method = 'set_' . $key;
           
$this->$method( $value );
                       
            foreach(
$this->_data as $k => $v )
            {                
                if( !
array_key_exists( $k, $oldValues ) or ( $v instanceof \IPS\Patterns\Bitwise and !( $oldValues[ $k ] instanceof \IPS\Patterns\Bitwise ) ) or $oldValues[ $k ] !== $v )
                {
                   
$this->changed[ $k ]    = $v;
                }
            }
           
            unset(
$oldValues );
        }
        else
        {
            if ( !
array_key_exists( $key, $this->_data ) or $this->_data[ $key ] !== $value )
            {
               
$this->changed[ $key ] = $value;
            }
           
           
$this->_data[ $key ] = $value;
        }
    }
   
   
/**
     * Is value in data store?
     *
     * @param    mixed    $key    Key
     * @return    bool
     */
   
public function __isset( $key )
    {
        if (
method_exists( $this, 'get_' . $key ) )
        {
           
$method = 'get_' . $key;
            return
$this->$method() !== NULL;
        }
       
        if ( isset(
$this->_data[$key] ) )
        {
            return
TRUE;
        }
       
        return
FALSE;
    }
   
   
/**
     * @brief    By default cloning will create a new ActiveRecord record, but if you truly want an object copy you can set this to TRUE first and a direct copy will be returned
     */
   
public $skipCloneDuplication    = FALSE;

   
/**
     * [ActiveRecord] Duplicate
     *
     * @return    void
     */
   
public function __clone()
    {
        if(
$this->skipCloneDuplication === TRUE )
        {
            return;
        }

       
$primaryKey = static::$databaseColumnId;
       
$this->$primaryKey = NULL;
       
       
$this->_new = TRUE;
       
$this->save();
    }
   
   
/**
     * Save Changed Columns
     *
     * @return    void
     */
   
public function save()
    {
        if (
$this->_new )
        {
           
$data = $this->_data;
        }
        else
        {
           
$data = $this->changed;
        }

        foreach (
array_keys( static::$bitOptions ) as $k )
        {            
            if (
$this->$k instanceof Bitwise )
            {
                foreach(
$this->$k->values as $field => $value )
                {
                    if ( isset(
$data[ $field ] ) or $this->$k->originalValues[ $field ] != intval( $value ) )
                    {
                       
$data[ $field ] = intval( $value );
                    }
                }
            }
        }

        if (
$this->_new )
        {
           
$insert = array();
            if( static::
$databasePrefix === NULL )
            {
               
$insert = $data;
            }
            else
            {
               
$insert = array();
                foreach (
$data as $k => $v )
                {
                   
$insert[ static::$databasePrefix . $k ] = $v;
                }
            }
           
           
$insertId = static::db()->insert( static::$databaseTable, $insert );
           
           
$primaryKey = static::$databaseColumnId;
            if (
$this->$primaryKey === NULL and $insertId )
            {
               
$this->$primaryKey = $insertId;
            }
           
           
$this->_new = FALSE;

           
/* Reset our log of what's changed */
           
$this->changed = array();

            static::
$multitons[ $this->$primaryKey ] = $this;
        }
        elseif( !empty(
$data ) )
        {
           
/* Set the column names with a prefix */
           
if( static::$databasePrefix === NULL )
            {
               
$update = $data;
            }
            else
            {
               
$update = array();

                foreach (
$data as $k => $v )
                {
                   
$update[ static::$databasePrefix . $k ] = $v;
                }
            }
                       
           
/* Save */
           
static::db()->update( static::$databaseTable, $update, $this->_whereClauseForSave() );
           
           
/* Reset our log of what's changed */
           
$this->changed = array();
        }
    }
   
   
/**
     * Get the WHERE clause for save()
     *
     * @return    void
     */
   
protected function _whereClauseForSave()
    {
       
$idColumn = static::$databaseColumnId;
        return array( static::
$databasePrefix . $idColumn . '=?', $this->$idColumn );
    }
   
   
/**
     * [ActiveRecord] Delete Record
     *
     * @return    void
     */
   
public function delete()
    {
       
$idColumn = static::$databaseColumnId;
        static::
db()->delete( static::$databaseTable, array( static::$databasePrefix . $idColumn . '=?', $this->$idColumn ) );
    }
   
   
/**
     * Get follow data
     *
     * @param    string                    $area            Area
     * @param    int                        $id                ID
     * @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
     * @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
     */
   
protected static function _followers( $area, $id, $privacy, $frequencyTypes, $date, $limit, $order=NULL, $flags=\IPS\Db::SELECT_SQL_CALC_FOUND_ROWS )
    {
       
/* Initial where clause */
       
$where[] = array( 'follow_app=? AND follow_area=? AND follow_rel_id=?', static::$application, $area, $id );
   
       
/* Public / Anonymous */
       
if ( !( $privacy & static::FOLLOW_PUBLIC ) )
        {
           
$where[] = array( 'follow_is_anon=1' );
        }
        elseif ( !(
$privacy & static::FOLLOW_ANONYMOUS ) )
        {
           
$where[] = array( 'follow_is_anon=0' );
        }
   
       
/* Specific type */
       
if ( $frequencyTypes != array( 'immediate', 'daily', 'weekly' ) )
        {
           
$where[] = array( \IPS\Db::i()->in( 'follow_notify_freq', $frequencyTypes ) );
        }
       
       
/* Since */
       
if( $date !== NULL )
        {
           
$where[] = array( 'follow_added<?', ( $date instanceof \IPS\DateTime ) ? $date->getTimestamp() : intval( $date ) );
        }

       
/* 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 );

        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' ) );
        }
        else
        {
           
$cache[ $_hash ]    = \IPS\Db::i()->select( 'core_follow.*', 'core_follow', $where, $order, $limit, NULL, NULL, $flags );
        }

        return
$cache[ $_hash ];
    }
   
   
/**
     * Cover Photo
     *
     * @return    \IPS\Helpers\CoverPhoto
     */
   
public function coverPhoto()
    {
       
$photoCol = static::$databaseColumnMap[ 'cover_photo' ];
       
$photoOffset = static::$databaseColumnMap[ 'cover_photo_offset' ];
       
$photo = new \IPS\Helpers\CoverPhoto;
        if ( isset( static::
$databaseColumnMap['cover_photo'] ) and $this->$photoCol )
        {
           
$photo->file = \IPS\File::get( static::$coverPhotoStorageExtension, $this->$photoCol );
           
$photo->offset = $this->$photoOffset;
        }
       
       
$photo->editable = $this->canEdit();
       
$photo->object = $this;

        return
$photo;
    }
   
   
/**
     * Produce a random hex color for a background
     *
     * @return string
     */
   
public function coverPhotoBackgroundColor()
    {
        return
'#' . dechex( mt_rand( 0x000000, 0xFFFFFF ) );
    }

   
/**
     * Return cover photo background color based on a string
     *
     * @param    string    $string    Some string to base background color on
     * @return    string
     */
   
protected function staticCoverPhotoBackgroundColor( $string )
    {
       
$integer    = 0;

        for(
$i=0, $j=\strlen($string); $i<$j; $i++ )
        {
           
$integer = ord( \substr( $string, $i, 1 ) ) + ( ( $integer << 5 ) - $integer );
           
$integer = $integer & $integer;
        }

        return
"hsl(" . ( $integer % 360 ) . ", 100%, 80% )";
    }
}