1156 lines
33 KiB
PHP
1156 lines
33 KiB
PHP
<?php
|
||
/**
|
||
* Created by PhpStorm.
|
||
* User: exodus4d
|
||
* Date: 16.02.15
|
||
* Time: 22:11
|
||
*/
|
||
|
||
namespace Model;
|
||
|
||
use DB\Cortex;
|
||
use DB\CortexCollection;
|
||
use DB\SQL\Schema;
|
||
use lib\Util;
|
||
use lib\logging;
|
||
use Controller;
|
||
use Exception\ValidationException;
|
||
use Exception\DatabaseException;
|
||
|
||
abstract class AbstractModel extends Cortex {
|
||
|
||
/**
|
||
* alias name for database connection
|
||
*/
|
||
const DB_ALIAS = '';
|
||
|
||
/**
|
||
* caching time of field schema - seconds
|
||
* as long as we don´t expect any field changes
|
||
* -> leave this at a higher value
|
||
* @var int
|
||
*/
|
||
protected $ttl = 60;
|
||
|
||
/**
|
||
* caching for relational data
|
||
* @var int
|
||
*/
|
||
protected $rel_ttl = 0;
|
||
|
||
/**
|
||
* ass static columns for this table
|
||
* -> can be overwritten in child models
|
||
* @var bool
|
||
*/
|
||
protected $addStaticFields = true;
|
||
|
||
/**
|
||
* enables table truncate
|
||
* -> see truncate();
|
||
* -> CAUTION! if set to true truncate() will clear ALL rows!
|
||
* @var bool
|
||
*/
|
||
protected $allowTruncate = false;
|
||
|
||
/**
|
||
* enables change for "active" column
|
||
* -> see setActive();
|
||
* -> $this->active = false; will NOT work (prevent abuse)!
|
||
* @var bool
|
||
*/
|
||
private $allowActiveChange = false;
|
||
|
||
/**
|
||
* getData() cache key prefix
|
||
* -> do not change, otherwise cached data is lost
|
||
* @var string
|
||
*/
|
||
private $dataCacheKeyPrefix = 'DATACACHE';
|
||
|
||
/**
|
||
* enables data export for this table
|
||
* -> can be overwritten in child models
|
||
* @var bool
|
||
*/
|
||
public static $enableDataExport = false;
|
||
|
||
/**
|
||
* enables data import for this table
|
||
* -> can be overwritten in child models
|
||
* @var bool
|
||
*/
|
||
public static $enableDataImport = false;
|
||
|
||
/**
|
||
* collection for validation errors
|
||
* @var array
|
||
*/
|
||
protected $validationError = [];
|
||
|
||
|
||
/**
|
||
* default charset for table
|
||
*/
|
||
const DEFAULT_CHARSET = 'utf8mb4';
|
||
|
||
/**
|
||
* default caching time of field schema - seconds
|
||
*/
|
||
const DEFAULT_TTL = 86400;
|
||
|
||
/**
|
||
* default TTL for getData(); cache - seconds
|
||
*/
|
||
const DEFAULT_CACHE_TTL = 120;
|
||
|
||
/**
|
||
* default TTL or temp table data read from *.csv file
|
||
* -> used during data import
|
||
*/
|
||
const DEFAULT_CACHE_CSV_TTL = 120;
|
||
|
||
/**
|
||
* cache key prefix name for "full table" indexing
|
||
* -> used e.g. for a "search" index; or "import" index for *.csv imports
|
||
*/
|
||
const CACHE_KEY_PREFIX = 'INDEX';
|
||
|
||
/**
|
||
* cache key name for temp data import from *.csv files per table
|
||
*/
|
||
const CACHE_KEY_CSV_PREFIX = 'CSV';
|
||
|
||
/**
|
||
* default TTL for SQL query cache
|
||
*/
|
||
const DEFAULT_SQL_TTL = 3;
|
||
|
||
/**
|
||
* data from Universe tables is static and does not change frequently
|
||
* -> refresh static data after X days
|
||
*/
|
||
const CACHE_MAX_DAYS = 60;
|
||
|
||
/**
|
||
* class not exists error
|
||
*/
|
||
const ERROR_INVALID_MODEL_CLASS = 'Model class (%s) not found';
|
||
|
||
/**
|
||
* AbstractModel constructor.
|
||
* @param null $db
|
||
* @param null $table
|
||
* @param null $fluid
|
||
* @param int $ttl
|
||
* @param string $charset
|
||
*/
|
||
public function __construct($db = null, $table = null, $fluid = null, $ttl = self::DEFAULT_TTL, $charset = self::DEFAULT_CHARSET){
|
||
|
||
if(!is_object($db)){
|
||
$db = self::getF3()->DB->getDB(static::DB_ALIAS);
|
||
}
|
||
|
||
if(is_null($db)){
|
||
// no valid DB connection found -> break on error
|
||
self::getF3()->set('HALT', true);
|
||
}
|
||
|
||
// set charset -> used during table setup()
|
||
$this->charset = $charset;
|
||
|
||
$this->addStaticFieldConfig();
|
||
|
||
parent::__construct($db, $table, $fluid, $ttl);
|
||
|
||
// insert events ------------------------------------------------------------------------------------
|
||
$this->beforeinsert(function($self, $pkeys){
|
||
return $self->beforeInsertEvent($self, $pkeys);
|
||
});
|
||
|
||
$this->afterinsert(function($self, $pkeys){
|
||
$self->afterInsertEvent($self, $pkeys);
|
||
});
|
||
|
||
// update events ------------------------------------------------------------------------------------
|
||
$this->beforeupdate(function($self, $pkeys){
|
||
return $self->beforeUpdateEvent($self, $pkeys);
|
||
});
|
||
|
||
$this->afterupdate(function($self, $pkeys){
|
||
$self->afterUpdateEvent($self, $pkeys);
|
||
});
|
||
|
||
// erase events -------------------------------------------------------------------------------------
|
||
$this->beforeerase(function($self, $pkeys){
|
||
return $self->beforeEraseEvent($self, $pkeys);
|
||
});
|
||
|
||
$this->aftererase(function($self, $pkeys){
|
||
$self->afterEraseEvent($self, $pkeys);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* checks whether table exists on DB
|
||
* @return bool
|
||
*/
|
||
public function tableExists() : bool {
|
||
return is_object($this->db) ? $this->db->tableExists($this->table) : false;
|
||
}
|
||
|
||
/**
|
||
* clear existing table Schema cache
|
||
* @return bool
|
||
*/
|
||
public function clearSchemaCache() : bool {
|
||
$f3 = self::getF3();
|
||
$cache=\Cache::instance();
|
||
if(
|
||
$f3->CACHE && is_object($this->db) &&
|
||
$cache->exists($hash = $f3->hash($this->db->getDSN() . $this->table) . '.schema')
|
||
){
|
||
return (bool)$cache->clear($hash);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* @param string $key
|
||
* @param mixed $val
|
||
* @return mixed
|
||
* @throws ValidationException
|
||
*/
|
||
public function set($key, $val){
|
||
if(is_string($val)){
|
||
$val = trim($val);
|
||
}
|
||
|
||
if(
|
||
!$this->dry() &&
|
||
$key != 'updated'
|
||
){
|
||
if($this->exists($key)){
|
||
// get raw column data (no objects)
|
||
$currentVal = $this->get($key, true);
|
||
|
||
if(is_object($val)){
|
||
if(
|
||
is_subclass_of($val, 'Model\AbstractModel') &&
|
||
$val->_id != $currentVal
|
||
){
|
||
// relational object changed
|
||
$this->touch('updated');
|
||
}
|
||
}elseif($val != $currentVal){
|
||
// non object value
|
||
$this->touch('updated');
|
||
}
|
||
}
|
||
}
|
||
|
||
if(!$this->validateField($key, $val)){
|
||
$this->throwValidationException($key);
|
||
}
|
||
|
||
return parent::set($key, $val);
|
||
}
|
||
|
||
/**
|
||
* setter for "active" status
|
||
* -> default: keep current "active" status
|
||
* -> can be overwritten
|
||
* @param bool $active
|
||
* @return mixed
|
||
*/
|
||
public function set_active($active){
|
||
if($this->allowActiveChange){
|
||
// allowed to set/change -> reset "allowed" property
|
||
$this->allowActiveChange = false;
|
||
}else{
|
||
// not allowed to set/change -> keep current status
|
||
$active = $this->active;
|
||
}
|
||
return $active;
|
||
}
|
||
|
||
/**
|
||
* get static fields for this model instance
|
||
* @return array
|
||
*/
|
||
protected function getStaticFieldConf() : array {
|
||
$staticFieldConfig = [];
|
||
|
||
// static tables (fixed data) do not require them...
|
||
if($this->addStaticFields){
|
||
$staticFieldConfig = [
|
||
'created' => [
|
||
'type' => Schema::DT_TIMESTAMP,
|
||
'default' => Schema::DF_CURRENT_TIMESTAMP,
|
||
'index' => true
|
||
],
|
||
'updated' => [
|
||
'type' => Schema::DT_TIMESTAMP,
|
||
'default' => Schema::DF_CURRENT_TIMESTAMP,
|
||
'index' => true
|
||
]
|
||
];
|
||
}
|
||
|
||
return $staticFieldConfig;
|
||
}
|
||
|
||
/**
|
||
* extent the fieldConf Array with static fields for each table
|
||
*/
|
||
private function addStaticFieldConfig(){
|
||
$this->fieldConf = array_merge($this->getStaticFieldConf(), $this->fieldConf);
|
||
}
|
||
|
||
/**
|
||
* validates a table column based on validation settings
|
||
* @param string $key
|
||
* @param $val
|
||
* @return bool
|
||
*/
|
||
protected function validateField(string $key, $val) : bool {
|
||
$valid = true;
|
||
if($fieldConf = $this->fieldConf[$key]){
|
||
if($method = $this->fieldConf[$key]['validate']){
|
||
if( !is_string($method)){
|
||
$method = $key;
|
||
}
|
||
$method = 'validate_' . $method;
|
||
if(method_exists($this, $method)){
|
||
// validate $key (column) with this method...
|
||
$valid = $this->$method($key, $val);
|
||
}else{
|
||
self::getF3()->error(501, 'Method ' . get_class($this) . '->' . $method . '() is not implemented');
|
||
};
|
||
}
|
||
}
|
||
|
||
return $valid;
|
||
}
|
||
|
||
/**
|
||
* validates a model field to be a valid relational model
|
||
* @param $key
|
||
* @param $val
|
||
* @return bool
|
||
* @throws ValidationException
|
||
*/
|
||
protected function validate_notDry($key, $val) : bool {
|
||
$valid = true;
|
||
if($colConf = $this->fieldConf[$key]){
|
||
if(isset($colConf['belongs-to-one'])){
|
||
if( (is_int($val) || ctype_digit($val)) && (int)$val > 0){
|
||
$valid = true;
|
||
}elseif( is_a($val, $colConf['belongs-to-one']) && !$val->dry() ){
|
||
$valid = true;
|
||
}else{
|
||
$valid = false;
|
||
$msg = 'Validation failed: "' . get_class($this) . '->' . $key . '" must be a valid instance of ' . $colConf['belongs-to-one'];
|
||
$this->throwValidationException($key, $msg);
|
||
}
|
||
}
|
||
}
|
||
|
||
return $valid;
|
||
}
|
||
|
||
/**
|
||
* validates a model field to be not empty
|
||
* @param $key
|
||
* @param $val
|
||
* @return bool
|
||
*/
|
||
protected function validate_notEmpty($key, $val) : bool {
|
||
$valid = false;
|
||
if($colConf = $this->fieldConf[$key]){
|
||
switch($colConf['type']){
|
||
case Schema::DT_INT:
|
||
case Schema::DT_FLOAT:
|
||
if( (is_int($val) || ctype_digit($val)) && (int)$val > 0){
|
||
$valid = true;
|
||
}
|
||
break;
|
||
case Schema::DT_VARCHAR128:
|
||
case Schema::DT_VARCHAR256:
|
||
case Schema::DT_VARCHAR512:
|
||
if(!empty($val)){
|
||
$valid = true;
|
||
}
|
||
break;
|
||
default:
|
||
}
|
||
}
|
||
|
||
return $valid;
|
||
}
|
||
|
||
/**
|
||
* get key for for all objects in this table
|
||
* @return string
|
||
*/
|
||
private function getTableCacheKey() : string {
|
||
return $this->dataCacheKeyPrefix .'.' . strtoupper($this->table);
|
||
}
|
||
|
||
/**
|
||
* get the cache key for this model
|
||
* ->do not set a key if the model is not saved!
|
||
* @param string $dataCacheTableKeyPrefix
|
||
* @return null|string
|
||
*/
|
||
protected function getCacheKey(string $dataCacheTableKeyPrefix = '') : ?string {
|
||
$cacheKey = null;
|
||
|
||
// set a model unique cache key if the model is saved
|
||
if($this->_id > 0){
|
||
$cacheKey = $this->getTableCacheKey();
|
||
|
||
// check if there is a given key prefix
|
||
// -> if not, use the standard key.
|
||
// this is useful for caching multiple data sets according to one row entry
|
||
if(!empty($dataCacheTableKeyPrefix)){
|
||
$cacheKey .= '.' . $dataCacheTableKeyPrefix . '_';
|
||
}else{
|
||
$cacheKey .= '.ID_';
|
||
}
|
||
$cacheKey .= (string)$this->_id;
|
||
}
|
||
|
||
return $cacheKey;
|
||
}
|
||
|
||
/**
|
||
* get cached data from this model
|
||
* @param string $dataCacheKeyPrefix - optional key prefix
|
||
* @return mixed|null
|
||
*/
|
||
protected function getCacheData($dataCacheKeyPrefix = ''){
|
||
$cacheData = null;
|
||
// table cache exists
|
||
// -> check cache for this row data
|
||
if(!is_null($cacheKey = $this->getCacheKey($dataCacheKeyPrefix))){
|
||
self::getF3()->exists($cacheKey, $cacheData);
|
||
}
|
||
return $cacheData;
|
||
}
|
||
|
||
/**
|
||
* update/set the getData() cache for this object
|
||
* @param $cacheData
|
||
* @param string $dataCacheKeyPrefix
|
||
* @param int $data_ttl
|
||
*/
|
||
public function updateCacheData($cacheData, string $dataCacheKeyPrefix = '', int $data_ttl = self::DEFAULT_CACHE_TTL){
|
||
$cacheDataTmp = (array)$cacheData;
|
||
|
||
// check if data should be cached
|
||
// and cacheData is not empty
|
||
if(
|
||
$data_ttl > 0 &&
|
||
!empty($cacheDataTmp)
|
||
){
|
||
$cacheKey = $this->getCacheKey($dataCacheKeyPrefix);
|
||
if(!is_null($cacheKey)){
|
||
self::getF3()->set($cacheKey, $cacheData, $data_ttl);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* unset the getData() cache for this object
|
||
* -> see also clearCacheDataWithPrefix(), for more information
|
||
*/
|
||
public function clearCacheData(){
|
||
$this->clearCache($this->getCacheKey());
|
||
}
|
||
|
||
/**
|
||
* unset object cached data by prefix
|
||
* -> primarily used by object cache with multiple data caches
|
||
* @param string $dataCacheKeyPrefix
|
||
*/
|
||
public function clearCacheDataWithPrefix(string $dataCacheKeyPrefix = ''){
|
||
$this->clearCache($this->getCacheKey($dataCacheKeyPrefix));
|
||
}
|
||
|
||
/**
|
||
* unset object cached data (if exists)
|
||
* @param $cacheKey
|
||
*/
|
||
private function clearCache($cacheKey){
|
||
if(!empty($cacheKey)){
|
||
$f3 = self::getF3();
|
||
if($f3->exists($cacheKey)){
|
||
$f3->clear($cacheKey);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* throw validation exception for a model property
|
||
* @param string $col
|
||
* @param string $msg
|
||
* @throws ValidationException
|
||
*/
|
||
protected function throwValidationException(string $col, string $msg = ''){
|
||
$msg = empty($msg) ? 'Validation failed: "' . $col . '".' : $msg;
|
||
throw new ValidationException($msg, $col);
|
||
}
|
||
|
||
/**
|
||
* @param string $msg
|
||
* @throws DatabaseException
|
||
*/
|
||
protected function throwDbException(string $msg){
|
||
throw new DatabaseException($msg);
|
||
}
|
||
|
||
/**
|
||
* checks whether this model is active or not
|
||
* each model should have an "active" column
|
||
* @return bool
|
||
*/
|
||
public function isActive() : bool {
|
||
return (bool)$this->active;
|
||
}
|
||
|
||
/**
|
||
* set active state for a model
|
||
* -> do NOT use $this->active for status change!
|
||
* -> this will not work (prevent abuse)
|
||
* @param bool $active
|
||
*/
|
||
public function setActive(bool $active){
|
||
// enables "active" change for this model
|
||
$this->allowActiveChange = true;
|
||
$this->active = $active;
|
||
}
|
||
|
||
/**
|
||
* get single dataSet by id
|
||
* @param int $id
|
||
* @param int $ttl
|
||
* @param bool $isActive
|
||
* @return bool
|
||
*/
|
||
public function getById(int $id, int $ttl = self::DEFAULT_SQL_TTL, bool $isActive = true) : bool {
|
||
return $this->getByForeignKey($this->primary, $id, ['limit' => 1], $ttl, $isActive);
|
||
}
|
||
|
||
/**
|
||
* get dataSet by foreign column (single result)
|
||
* @param string $key
|
||
* @param $value
|
||
* @param array $options
|
||
* @param int $ttl
|
||
* @param bool $isActive
|
||
* @return bool
|
||
*/
|
||
public function getByForeignKey(string $key, $value, array $options = [], int $ttl = 0, bool $isActive = true) : bool {
|
||
$filters = [self::getFilter($key, $value)];
|
||
|
||
if($isActive && $this->exists('active')){
|
||
$filters[] = self::getFilter('active', true);
|
||
}
|
||
|
||
$this->filterRel();
|
||
|
||
return $this->load($this->mergeFilter($filters), $options, $ttl);
|
||
}
|
||
|
||
/**
|
||
* apply filter() for relations
|
||
* -> overwrite in child classes
|
||
* @see https://github.com/ikkez/f3-cortex#filter
|
||
*/
|
||
protected function filterRel() : void {}
|
||
|
||
/**
|
||
* get first model from a relation that matches $filter
|
||
* @param string $key
|
||
* @param array $filter
|
||
* @return mixed|null
|
||
*/
|
||
protected function relFindOne(string $key, array $filter){
|
||
$relModel = null;
|
||
$relFilter = [];
|
||
if($this->exists($key, true)){
|
||
$fieldConf = $this->getFieldConfiguration();
|
||
if(array_key_exists($key, $fieldConf)){
|
||
if(array_key_exists($type = 'has-many', $fieldConf[$key])){
|
||
$fromConf = $fieldConf[$key][$type];
|
||
$relFilter = self::getFilter($fromConf[1], $this->getRaw($fromConf['relField']));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @var $relModel self|bool
|
||
*/
|
||
$relModel = $this->rel($key)->findone($this->mergeFilter([$relFilter, $this->mergeWithRelFilter($key, $filter)]));
|
||
}
|
||
|
||
return $relModel ? : null;
|
||
}
|
||
|
||
/**
|
||
* get all models from a relation that match $filter
|
||
* @param string $key
|
||
* @param array $filter
|
||
* @return CortexCollection|null
|
||
*/
|
||
protected function relFind(string $key, array $filter) : ?CortexCollection {
|
||
$relModel = null;
|
||
$relFilter = [];
|
||
if($this->exists($key, true)){
|
||
$fieldConf = $this->getFieldConfiguration();
|
||
if(array_key_exists($key, $fieldConf)){
|
||
if(array_key_exists($type = 'has-many', $fieldConf[$key])){
|
||
$fromConf = $fieldConf[$key][$type];
|
||
$relFilter = self::getFilter($fromConf[1], $this->getRaw($fromConf['relField']));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @var $relModel CortexCollection|bool
|
||
*/
|
||
$relModel = $this->rel($key)->find($this->mergeFilter([$relFilter, $this->mergeWithRelFilter($key, $filter)]));
|
||
}
|
||
|
||
return $relModel ? : null;
|
||
}
|
||
|
||
/**
|
||
* Event "Hook" function
|
||
* can be overwritten
|
||
* return false will stop any further action
|
||
* @param self $self
|
||
* @param $pkeys
|
||
* @return bool
|
||
*/
|
||
public function beforeInsertEvent($self, $pkeys) : bool {
|
||
if($this->exists('updated')){
|
||
$this->touch('updated');
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Event "Hook" function
|
||
* can be overwritten
|
||
* return false will stop any further action
|
||
* @param self $self
|
||
* @param $pkeys
|
||
*/
|
||
public function afterInsertEvent($self, $pkeys){
|
||
}
|
||
|
||
/**
|
||
* Event "Hook" function
|
||
* can be overwritten
|
||
* return false will stop any further action
|
||
* @param self $self
|
||
* @param $pkeys
|
||
* @return bool
|
||
*/
|
||
public function beforeUpdateEvent($self, $pkeys) : bool {
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Event "Hook" function
|
||
* can be overwritten
|
||
* return false will stop any further action
|
||
* @param self $self
|
||
* @param $pkeys
|
||
*/
|
||
public function afterUpdateEvent($self, $pkeys){
|
||
}
|
||
|
||
/**
|
||
* Event "Hook" function
|
||
* can be overwritten
|
||
* @param self $self
|
||
* @param $pkeys
|
||
* @return bool
|
||
*/
|
||
public function beforeEraseEvent($self, $pkeys) : bool {
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Event "Hook" function
|
||
* can be overwritten
|
||
* @param self $self
|
||
* @param $pkeys
|
||
*/
|
||
public function afterEraseEvent($self, $pkeys){
|
||
}
|
||
|
||
/**
|
||
* function should be overwritten in parent classes
|
||
* @return bool
|
||
*/
|
||
public function isValid() : bool {
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* get row count in this table
|
||
* @return int
|
||
*/
|
||
public function getRowCount() : int {
|
||
return is_object($this->db) ? $this->db->getRowCount($this->getTable()) : 0;
|
||
}
|
||
|
||
/**
|
||
* truncate all table rows
|
||
* -> Use with Caution!!!
|
||
*/
|
||
public function truncate(){
|
||
if($this->allowTruncate && is_object($this->db)){
|
||
$this->db->exec("TRUNCATE " . $this->getTable());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* format dateTime column
|
||
* @param $column
|
||
* @param string $format
|
||
* @return false|null|string
|
||
*/
|
||
public function getFormattedColumn($column, $format = 'Y-m-d H:i'){
|
||
return $this->get($column) ? date($format, strtotime( $this->get($column) )) : null;
|
||
}
|
||
|
||
/**
|
||
* export and download table data as *.csv
|
||
* this is primarily used for static tables
|
||
* @param array $fields
|
||
* @return bool
|
||
*/
|
||
public function exportData(array $fields = []) : bool {
|
||
$status = false;
|
||
|
||
if(static::$enableDataExport){
|
||
$tableModifier = static::getTableModifier();
|
||
$headers = $tableModifier->getCols();
|
||
|
||
if($fields){
|
||
// columns to export -> reIndex keys
|
||
$headers = array_values(array_intersect($headers, $fields));
|
||
}
|
||
|
||
// just get the records with existing columns
|
||
// -> no "virtual" fields or "new" columns
|
||
$this->fields($headers);
|
||
$allRecords = $this->find();
|
||
|
||
if($allRecords){
|
||
$tableData = $allRecords->castAll(0);
|
||
|
||
// format data -> "id" must be first key
|
||
foreach($tableData as &$rowData){
|
||
$rowData = [$this->primary => $rowData['_id']] + $rowData;
|
||
unset($rowData['_id']);
|
||
}
|
||
|
||
$sheet = \Sheet::instance();
|
||
$data = $sheet->dumpCSV($tableData, $headers);
|
||
|
||
header('Expires: 0');
|
||
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
|
||
header('Content-Type: text/csv;charset=UTF-8');
|
||
header('Content-Disposition: attachment;filename=' . $this->getTable() . '.csv');
|
||
echo $data;
|
||
exit();
|
||
}
|
||
}
|
||
|
||
return $status;
|
||
}
|
||
|
||
/**
|
||
* read *.csv file for a $table name
|
||
* -> 'group' by $getByKey column name and return array
|
||
* @param string $table
|
||
* @param string $getByKey
|
||
* @return array
|
||
*/
|
||
public static function getCSVData(string $table, string $getByKey = 'id') : array {
|
||
$hashKeyTableCSV = static::generateHashKeyTable($table, static::CACHE_KEY_PREFIX . '_' . self::CACHE_KEY_CSV_PREFIX);
|
||
|
||
if(
|
||
!self::getF3()->exists($hashKeyTableCSV, $tableData) &&
|
||
!empty($tableData = Util::arrayGetBy(self::loadCSV($table), $getByKey, false))
|
||
){
|
||
self::getF3()->set($hashKeyTableCSV, $tableData, self::DEFAULT_CACHE_CSV_TTL);
|
||
}
|
||
|
||
return $tableData;
|
||
}
|
||
|
||
/**
|
||
* load data from *.csv file
|
||
* @param string $fileName
|
||
* @return array
|
||
*/
|
||
protected static function loadCSV(string $fileName) : array {
|
||
$tableData = [];
|
||
|
||
// rtrim(); for arrays (removes empty values) from the end
|
||
$rtrim = function($array = [], $lengthMin = false) : array {
|
||
$length = key(array_reverse(array_diff($array, ['']), 1))+1;
|
||
$length = $length < $lengthMin ? $lengthMin : $length;
|
||
return array_slice($array, 0, $length);
|
||
};
|
||
|
||
if($fileName){
|
||
$filePath = self::getF3()->get('EXPORT') . 'csv/' . $fileName . '.csv';
|
||
if(is_file($filePath)){
|
||
$handle = @fopen($filePath, 'r');
|
||
$keys = array_map('lcfirst', fgetcsv($handle, 0, ';'));
|
||
$keys = $rtrim($keys);
|
||
|
||
if(count($keys) > 0){
|
||
while (!feof($handle)) {
|
||
$tableData[] = array_combine($keys, $rtrim(fgetcsv($handle, 0, ';'), count($keys)));
|
||
}
|
||
}else{
|
||
self::getF3()->error(500, 'File could not be read');
|
||
}
|
||
}else{
|
||
self::getF3()->error(404, 'File not found: ' . $filePath);
|
||
}
|
||
}
|
||
|
||
return $tableData;
|
||
}
|
||
|
||
/**
|
||
* import table data from a *.csv file
|
||
* @return array|bool
|
||
*/
|
||
public function importData(){
|
||
$status = false;
|
||
|
||
if(
|
||
static::$enableDataImport &&
|
||
!empty($tableData = self::loadCSV($this->getTable()))
|
||
){
|
||
// import row data
|
||
$status = $this->importStaticData($tableData);
|
||
$this->getF3()->status(202);
|
||
}
|
||
|
||
return $status;
|
||
}
|
||
|
||
/**
|
||
* insert/update static data into this table
|
||
* WARNING: rows will be deleted if not part of $tableData !
|
||
* @param array $tableData
|
||
* @return array
|
||
*/
|
||
protected function importStaticData(array $tableData = []) : array {
|
||
$rowIDs = [];
|
||
$addedCount = 0;
|
||
$updatedCount = 0;
|
||
$deletedCount = 0;
|
||
|
||
$tableModifier = static::getTableModifier();
|
||
$fields = $tableModifier->getCols();
|
||
|
||
foreach($tableData as $rowData){
|
||
// search for existing record and update columns
|
||
$this->getById($rowData['id'], 0);
|
||
if($this->dry()){
|
||
$addedCount++;
|
||
}else{
|
||
$updatedCount++;
|
||
}
|
||
$this->copyfrom($rowData, $fields);
|
||
$this->save();
|
||
$rowIDs[] = $this->_id;
|
||
$this->reset();
|
||
}
|
||
|
||
// remove old data
|
||
$oldRows = $this->find('id NOT IN (' . implode(',', $rowIDs) . ')');
|
||
if($oldRows){
|
||
foreach($oldRows as $oldRow){
|
||
$oldRow->erase();
|
||
$deletedCount++;
|
||
}
|
||
}
|
||
return ['added' => $addedCount, 'updated' => $updatedCount, 'deleted' => $deletedCount];
|
||
}
|
||
|
||
/**
|
||
* get "default" logging object for this kind of model
|
||
* -> can be overwritten
|
||
* @param string $action
|
||
* @return Logging\LogInterface
|
||
*/
|
||
protected function newLog(string $action = '') : Logging\LogInterface{
|
||
return new Logging\DefaultLog($action);
|
||
}
|
||
|
||
/**
|
||
* get formatter callback function for parsed logs
|
||
* @return null
|
||
*/
|
||
protected function getLogFormatter(){
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* add new validation error
|
||
* @param ValidationException $e
|
||
*/
|
||
protected function setValidationError(ValidationException $e){
|
||
$this->validationError[] = $e->getError();
|
||
}
|
||
|
||
/**
|
||
* get all validation errors
|
||
* @return array
|
||
*/
|
||
public function getErrors() : array {
|
||
return $this->validationError;
|
||
}
|
||
|
||
/**
|
||
* checks whether data is outdated and should be refreshed
|
||
* @return bool
|
||
*/
|
||
protected function isOutdated() : bool {
|
||
$outdated = true;
|
||
if($this->valid()){
|
||
try{
|
||
$timezone = $this->getF3()->get('getTimeZone')();
|
||
$currentTime = new \DateTime('now', $timezone);
|
||
$updateTime = \DateTime::createFromFormat(
|
||
'Y-m-d H:i:s',
|
||
$this->updated,
|
||
$timezone
|
||
);
|
||
$interval = $updateTime->diff($currentTime);
|
||
if($interval->days < self::CACHE_MAX_DAYS){
|
||
$outdated = false;
|
||
}
|
||
}catch(\Exception $e){
|
||
self::getF3()->error($e->getCode(), $e->getMessage(), $e->getTrace());
|
||
}
|
||
}
|
||
return $outdated;
|
||
}
|
||
|
||
/**
|
||
* @return mixed
|
||
*/
|
||
public function save(){
|
||
$return = false;
|
||
try{
|
||
$return = parent::save();
|
||
}catch(ValidationException $e){
|
||
$this->setValidationError($e);
|
||
}catch(DatabaseException $e){
|
||
self::getF3()->error($e->getResponseCode(), $e->getMessage(), $e->getTrace());
|
||
}
|
||
|
||
return $return;
|
||
}
|
||
|
||
/**
|
||
* @return string
|
||
*/
|
||
public function __toString() : string {
|
||
return $this->getTable();
|
||
}
|
||
|
||
/**
|
||
* @param string $argument
|
||
* @return \ReflectionClass
|
||
* @throws \ReflectionException
|
||
*/
|
||
protected static function refClass($argument = self::class) : \ReflectionClass {
|
||
return new \ReflectionClass($argument);
|
||
}
|
||
|
||
/**
|
||
* get the framework instance
|
||
* @return \Base
|
||
*/
|
||
public static function getF3() : \Base {
|
||
return \Base::instance();
|
||
}
|
||
|
||
/**
|
||
* get model data as array
|
||
* @param $data
|
||
* @return array
|
||
*/
|
||
public static function toArray($data) : array {
|
||
return json_decode(json_encode($data), true);
|
||
}
|
||
|
||
/**
|
||
* get new filter array representation
|
||
* -> $suffix can be used fore unique placeholder,
|
||
* in case the same $key is used with different $values in the same query
|
||
* @param string $key
|
||
* @param mixed $value
|
||
* @param string $operator
|
||
* @param string $suffix
|
||
* @return array
|
||
*/
|
||
public static function getFilter(string $key, $value, string $operator = '=', string $suffix = '') : array {
|
||
$placeholder = ':' . implode('_', array_filter([$key, $suffix]));
|
||
return [$key . ' ' . $operator . ' ' . $placeholder, $placeholder => $value];
|
||
}
|
||
|
||
/**
|
||
* stores data direct into the Cache backend (e.g. Redis)
|
||
* $f3->set() used the same code. The difference is, that $f3->set()
|
||
* also loads data into the Hive.
|
||
* This can result in high RAM usage if a great number of key->values should be stored in Cache
|
||
* (like the search index for system data)
|
||
* @param string $key
|
||
* @param $data
|
||
* @param int $ttl
|
||
*/
|
||
public static function setCacheValue(string $key, $data, int $ttl = 0){
|
||
$cache = \Cache::instance();
|
||
$cache->set(self::getF3()->hash($key).'.var', $data, $ttl);
|
||
}
|
||
|
||
/**
|
||
* check whether a cache $key exists
|
||
* -> §val (reference) get updated with the cache data
|
||
* -> equivalent to $f3->exists()
|
||
* @param string $key
|
||
* @param null $val
|
||
* @return bool
|
||
*/
|
||
public static function existsCacheValue(string $key, &$val = null){
|
||
$cache = \Cache::instance();
|
||
return $cache->exists(self::getF3()->hash($key).'.var',$val);
|
||
}
|
||
|
||
/**
|
||
* debug log function
|
||
* @param string $text
|
||
* @param string $type
|
||
*/
|
||
public static function log($text, $type = 'DEBUG'){
|
||
Controller\LogController::getLogger($type)->write($text);
|
||
}
|
||
|
||
/**
|
||
* get tableModifier class for this table
|
||
* @return bool|\DB\SQL\TableModifier
|
||
*/
|
||
public static function getTableModifier(){
|
||
$df = parent::resolveConfiguration();
|
||
$schema = new Schema($df['db']);
|
||
$tableModifier = $schema->alterTable($df['table']);
|
||
return $tableModifier;
|
||
}
|
||
|
||
/**
|
||
* Check whether a (multi)-column index exists or not on a table
|
||
* related to this model
|
||
* @param array $columns
|
||
* @return bool|array
|
||
*/
|
||
public static function indexExists(array $columns = []){
|
||
$tableModifier = self::getTableModifier();
|
||
$df = parent::resolveConfiguration();
|
||
|
||
$check = false;
|
||
$indexKey = $df['table'] . '___' . implode('__', $columns);
|
||
$indexList = $tableModifier->listIndex();
|
||
if(array_key_exists( $indexKey, $indexList)){
|
||
$check = $indexList[$indexKey];
|
||
}
|
||
|
||
return $check;
|
||
}
|
||
|
||
/**
|
||
* set a multi-column index for this table
|
||
* @param array $columns Column(s) to be indexed
|
||
* @param bool $unique Unique index
|
||
* @param int $length index length for text fields in mysql
|
||
* @return bool
|
||
*/
|
||
public static function setMultiColumnIndex(array $columns = [], $unique = false, $length = 20) : bool {
|
||
$status = false;
|
||
$tableModifier = self::getTableModifier();
|
||
|
||
if( self::indexExists($columns) === false ){
|
||
$tableModifier->addIndex($columns, $unique, $length);
|
||
$buildStatus = $tableModifier->build();
|
||
if($buildStatus === 0){
|
||
$status = true;
|
||
}
|
||
}
|
||
|
||
return $status;
|
||
}
|
||
|
||
/**
|
||
* factory for all Models
|
||
* @param string $className
|
||
* @param int $ttl
|
||
* @return AbstractModel|null
|
||
* @throws \Exception
|
||
*/
|
||
public static function getNew(string $className, int $ttl = self::DEFAULT_TTL) : ?self {
|
||
$model = null;
|
||
$className = self::refClass(static::class)->getNamespaceName() . '\\' . $className;
|
||
if(class_exists($className)){
|
||
$model = new $className(null, null, null, $ttl);
|
||
}else{
|
||
throw new \Exception(sprintf(self::ERROR_INVALID_MODEL_CLASS, $className));
|
||
}
|
||
return $model;
|
||
}
|
||
|
||
/**
|
||
* generate hashKey for a complete table
|
||
* -> should hold hashKeys for multiple rows
|
||
* @param string $table
|
||
* @param string $prefix
|
||
* @return string
|
||
*/
|
||
public static function generateHashKeyTable(string $table, string $prefix = self::CACHE_KEY_PREFIX ) : string {
|
||
return $prefix . '_' . strtolower($table);
|
||
}
|
||
|
||
/**
|
||
* overwrites parent
|
||
* @param null $db
|
||
* @param null $table
|
||
* @param null $fields
|
||
* @return bool
|
||
* @throws \Exception
|
||
*/
|
||
public static function setup($db = null, $table = null, $fields = null){
|
||
$status = parent::setup($db, $table, $fields);
|
||
|
||
// set static default data
|
||
if($status === true && property_exists(static::class, 'tableData')){
|
||
$model = self::getNew(self::refClass(static::class)->getShortName(), 0);
|
||
$model->importStaticData(static::$tableData);
|
||
}
|
||
|
||
return $status;
|
||
}
|
||
|
||
}
|