diff --git a/.gitignore b/.gitignore
index e1ca2a9fa..3c7be4fad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@ config/routing/*
composer.phar
composer.lock
.vscode
+.idea
public/*
!public/index.php
!public/console.php
diff --git a/config/config-example_mysql.json b/config/config-example_mysql.json
index 6d7f505f4..18f3a2e1c 100644
--- a/config/config-example_mysql.json
+++ b/config/config-example_mysql.json
@@ -1,8 +1,8 @@
{
"ROUTING_METHOD": "JSON",
"FILE_STORAGE_MODE":"DB",
- "DEFAULT_RIGHTS": "QN_R_CREATE | QN_R_READ | QN_R_DELETE | QN_R_WRITE",
- "DEBUG_MODE": "QN_MODE_PHP | QN_MODE_ORM | QN_MODE_SQL",
+ "DEFAULT_RIGHTS": "EQ_R_CREATE | EQ_R_READ | EQ_R_DELETE | EQ_R_WRITE",
+ "DEBUG_MODE": "EQ_MODE_API | EQ_MODE_PHP | EQ_MODE_ORM | EQ_MODE_SQL",
"DEBUG_LEVEL": "E_ALL | E_ALL",
"DEFAULT_LANG": "en",
"DB_REPLICATION": "NO",
diff --git a/config/config-example_sqlsrv.json b/config/config-example_sqlsrv.json
index a8592af07..5c1181d4b 100644
--- a/config/config-example_sqlsrv.json
+++ b/config/config-example_sqlsrv.json
@@ -2,8 +2,8 @@
"CIPHER_KEY": "xxxxxxxxxxxxxxxxxxxxxxxx",
"ROUTING_METHOD": "JSON",
"FILE_STORAGE_MODE":"DB",
- "DEFAULT_RIGHTS": "QN_R_CREATE | QN_R_READ | QN_R_DELETE | QN_R_WRITE",
- "DEBUG_MODE": "QN_MODE_PHP | QN_MODE_ORM | QN_MODE_SQL",
+ "DEFAULT_RIGHTS": "EQ_R_CREATE | EQ_R_READ | EQ_R_DELETE | EQ_R_WRITE",
+ "DEBUG_MODE": "EQ_MODE_PHP | EQ_MODE_ORM | EQ_MODE_SQL",
"DEBUG_LEVEL": "E_ALL | E_ALL",
"DEFAULT_LANG": "en",
"DB_REPLICATION": "NO",
diff --git a/config/schema.json b/config/schema.json
index 387b40478..36deabb63 100644
--- a/config/schema.json
+++ b/config/schema.json
@@ -15,7 +15,7 @@
},
"DEFAULT_LANG": {
"type": "string",
- "description": "The language in which the multilang content must be provided/stored by default (ISO 639-1).",
+ "description": "The language (ISO 639-1) in which the multilang content must be provided/stored by default.",
"instant": true,
"default": "en"
},
@@ -50,8 +50,8 @@
"default": 0,
"examples": [
0,
- "QN_R_READ | QN_R_WRITE",
- "QN_R_CREATE | QN_R_READ | QN_R_DELETE | QN_R_WRITE | QN_R_MANAGE"
+ "EQ_R_READ | EQ_R_WRITE",
+ "EQ_R_CREATE | EQ_R_READ | EQ_R_DELETE | EQ_R_WRITE | EQ_R_MANAGE"
]
},
"DEBUG_MODE": {
@@ -60,16 +60,18 @@
"instant": true,
"default": 0,
"examples": [
- "QN_MODE_PHP | QN_MODE_ORM | QN_MODE_SQL | QN_MODE_APP | QN_MODE_API"
+ "EQ_MODE_PHP | EQ_MODE_ORM | EQ_MODE_SQL | EQ_MODE_APP | EQ_MODE_API | EQ_MODE_AAA | EQ_MODE_NET"
]
},
"DEBUG_LEVEL": {
"type": "integer",
- "description": "Types of error to report (defaults to E_ALL = 32767, setting to 0 means no logs). Important: if logging level is set to E_ALL with all mode enabled, then the log file will grow quickly.",
+ "description": "Types of error to report (defaults to E_ALL = 32767, setting to 0 means no logs). Important: if logging level is set to E_ALL with all mode enabled, then the log file grows quite quickly.",
"instant": true,
"default": 0,
"examples": [
- "QN_REPORT_DEBUG | QN_REPORT_INFO | QN_REPORT_WARNING | QN_REPORT_ERROR | QN_REPORT_FATAL"
+ "EQ_REPORT_DEBUG | EQ_REPORT_INFO | EQ_REPORT_WARNING | EQ_REPORT_ERROR | EQ_REPORT_FATAL",
+ "EQ_REPORT_INFO | EQ_REPORT_WARNING | EQ_REPORT_ERROR",
+ "EQ_REPORT_DEBUG & E_ALL"
]
},
"HTTP_REQUEST_TIMEOUT": {
@@ -282,8 +284,12 @@
},
"USER_ACCOUNT_DISPLAYNAME": {
"type": "string",
- "description": "Strategy for displaying the user name to other users. The expected value is a string holding one or more of following references: 'id', 'nickname', 'mail', 'givenname', 'surname', 'initials' (Note: 'mail' is the email address and suits Apps restricted to users from a same organisation but should be avoided for non public profiles).",
- "default": "mail"
+ "description": "Strategy for displaying the user name to other users. The expected value is a string holding one or more of following references: 'id', 'nickname', 'mail', 'firstname', 'lastname', 'initials' (Note: 'mail' is the email address and suits Apps restricted to users from a same organisation but should be avoided for non public profiles).",
+ "default": "mail",
+ "examples": [
+ "nickname",
+ "firstname lastname"
+ ]
},
"USER_ACCOUNT_REGISTRATION": {
"type": "boolean",
diff --git a/doc b/doc
index 6f04d0ad5..a50423244 160000
--- a/doc
+++ b/doc
@@ -1 +1 @@
-Subproject commit 6f04d0ad57feb1c34b31738381ff9da7030dcc2c
+Subproject commit a504232442ffb7acc2c669b7cab848e134978c88
diff --git a/eq.lib.php b/eq.lib.php
index c2ad543a9..91d2a6994 100644
--- a/eq.lib.php
+++ b/eq.lib.php
@@ -38,56 +38,78 @@
/**
* Root directory of current install
*/
- define('QN_BASEDIR', realpath(dirname(__FILE__)));
+ define('EQ_BASEDIR', realpath(dirname(__FILE__)));
+ // equivalence for constant names migration
+ // #deprecated
+ define('QN_BASEDIR', EQ_BASEDIR);
/**
* Error codes
* we use negative values to make it possible to distinguish error codes from object ids
*/
- define('QN_ERROR_UNKNOWN', -1); // something went wrong (check the logs)
- define('QN_ERROR_MISSING_PARAM', -2); // one or more mandatory parameters are missing
- define('QN_ERROR_INVALID_PARAM', -4); // one or more parameters have invalid or incompatible value
- define('QN_ERROR_SQL', -8); // error while building SQL query or processing it (check that object class matches DB schema)
- define('QN_ERROR_UNKNOWN_OBJECT', -16); // unknown resource (class, object, view, ...)
- define('QN_ERROR_NOT_ALLOWED', -32); // action violates some rule (including UPLOAD_MAX_FILE_SIZE for binary fields) or user don't have required permissions
- define('QN_ERROR_LOCKED_OBJECT', -64); // object is currently locked by another process
- define('QN_ERROR_CONFLICT_OBJECT', -128); // version conflict
- define('QN_ERROR_INVALID_USER', -256); // auth failure
- define('QN_ERROR_UNKNOWN_SERVICE', -512); // server error : missing service
- define('QN_ERROR_INVALID_CONFIG', -1024); // server error : faulty configuration
-
-
+ define('EQ_ERROR_UNKNOWN', -1); // something went wrong (check the logs)
+ define('EQ_ERROR_MISSING_PARAM', -2); // one or more mandatory parameters are missing
+ define('EQ_ERROR_INVALID_PARAM', -4); // one or more parameters have invalid or incompatible value
+ define('EQ_ERROR_SQL', -8); // error while building SQL query or processing it (check that object class matches DB schema)
+ define('EQ_ERROR_UNKNOWN_OBJECT', -16); // unknown resource (class, object, view, ...)
+ define('EQ_ERROR_NOT_ALLOWED', -32); // action violates some rule (including UPLOAD_MAX_FILE_SIZE for binary fields) or user don't have required permissions
+ define('EQ_ERROR_LOCKED_OBJECT', -64); // object is currently locked by another process
+ define('EQ_ERROR_CONFLICT_OBJECT', -128); // version conflict
+ define('EQ_ERROR_INVALID_USER', -256); // auth failure
+ define('EQ_ERROR_UNKNOWN_SERVICE', -512); // server error : missing service
+ define('EQ_ERROR_INVALID_CONFIG', -1024); // server error : faulty configuration
// equivalence map for constant names migration
- define('EQ_ERROR_UNKNOWN', QN_ERROR_UNKNOWN);
- define('EQ_ERROR_MISSING_PARAM', QN_ERROR_MISSING_PARAM);
- define('EQ_ERROR_INVALID_PARAM', QN_ERROR_INVALID_PARAM);
- define('EQ_ERROR_SQL', QN_ERROR_SQL);
- define('EQ_ERROR_UNKNOWN_OBJECT', QN_ERROR_UNKNOWN_OBJECT);
- define('EQ_ERROR_NOT_ALLOWED', QN_ERROR_NOT_ALLOWED);
- define('EQ_ERROR_LOCKED_OBJECT', QN_ERROR_LOCKED_OBJECT);
- define('EQ_ERROR_CONFLICT_OBJECT', QN_ERROR_CONFLICT_OBJECT);
- define('EQ_ERROR_INVALID_USER', QN_ERROR_INVALID_USER);
- define('EQ_ERROR_UNKNOWN_SERVICE', QN_ERROR_UNKNOWN_SERVICE);
- define('EQ_ERROR_INVALID_CONFIG', QN_ERROR_INVALID_CONFIG);
+ // #deprecated
+ define('QN_ERROR_UNKNOWN', EQ_ERROR_UNKNOWN);
+ define('QN_ERROR_MISSING_PARAM', EQ_ERROR_MISSING_PARAM);
+ define('QN_ERROR_INVALID_PARAM', EQ_ERROR_INVALID_PARAM);
+ define('QN_ERROR_SQL', EQ_ERROR_SQL);
+ define('QN_ERROR_UNKNOWN_OBJECT', EQ_ERROR_UNKNOWN_OBJECT);
+ define('QN_ERROR_NOT_ALLOWED', EQ_ERROR_NOT_ALLOWED);
+ define('QN_ERROR_LOCKED_OBJECT', EQ_ERROR_LOCKED_OBJECT);
+ define('QN_ERROR_CONFLICT_OBJECT', EQ_ERROR_CONFLICT_OBJECT);
+ define('QN_ERROR_INVALID_USER', EQ_ERROR_INVALID_USER);
+ define('QN_ERROR_UNKNOWN_SERVICE', EQ_ERROR_UNKNOWN_SERVICE);
+ define('QN_ERROR_INVALID_CONFIG', EQ_ERROR_INVALID_CONFIG);
/**
* Debugging modes and levels
*/
// debugging modes
- define('QN_MODE_PHP', 1);
- define('QN_MODE_SQL', 2);
- define('QN_MODE_ORM', 4);
- define('QN_MODE_API', 8);
- define('QN_MODE_APP', 16);
+ define('EQ_MODE_PHP', 1); // low-level logs (code)
+ define('EQ_MODE_SQL', 2); // DB related logs
+ define('EQ_MODE_ORM', 4); // ORM entities & manipulations logs
+ define('EQ_MODE_API', 8); // routes & controllers related logs
+ define('EQ_MODE_APP', 16); // application logic logs
+ define('EQ_MODE_AAA', 32); // authentication, authorization & accounting logs
+ define('EQ_MODE_NET', 64); // network logs (tcp/ip, http)
+ // equivalence map for constant names migration
+ // #deprecated
+ define('QN_MODE_PHP', EQ_MODE_PHP);
+ define('QN_MODE_SQL', EQ_MODE_SQL);
+ define('QN_MODE_ORM', EQ_MODE_ORM);
+ define('QN_MODE_API', EQ_MODE_API);
+ define('QN_MODE_APP', EQ_MODE_APP);
+ define('QN_MODE_AAA', EQ_MODE_AAA);
+ define('QN_MODE_NET', EQ_MODE_NET);
// debugging levels
- define('QN_REPORT_DEBUG', E_USER_DEPRECATED); // 16384
- define('QN_REPORT_INFO', E_USER_NOTICE); // 1024
- define('QN_REPORT_WARNING', E_USER_WARNING); // 512
- define('QN_REPORT_ERROR', E_USER_ERROR); // 256
- define('QN_REPORT_FATAL', E_ERROR); // 1
+ define('EQ_REPORT_DEBUG', E_USER_DEPRECATED); // 16384
+ define('EQ_REPORT_INFO', E_USER_NOTICE); // 1024
+ define('EQ_REPORT_WARNING', E_USER_WARNING); // 512
+ define('EQ_REPORT_ERROR', E_USER_ERROR); // 256
+ define('EQ_REPORT_FATAL', E_ERROR); // 1
+ define('EQ_REPORT_SYSTEM', 0); // 0
+ // equivalence map for constant names migration
+ // #deprecated
+ define('QN_REPORT_DEBUG', EQ_REPORT_DEBUG);
+ define('QN_REPORT_INFO', EQ_REPORT_INFO);
+ define('QN_REPORT_WARNING', EQ_REPORT_WARNING);
+ define('QN_REPORT_ERROR', EQ_REPORT_ERROR);
+ define('QN_REPORT_FATAL', EQ_REPORT_FATAL);
+ define('QN_REPORT_SYSTEM', EQ_REPORT_SYSTEM);
/**
* Logs storage directory
@@ -204,6 +226,7 @@ function qn_debug_code_name($code) {
case QN_REPORT_WARNING: return 'WARNING';
case QN_REPORT_ERROR: return 'ERROR';
case QN_REPORT_FATAL: return 'FATAL';
+ case QN_REPORT_SYSTEM: return 'SYSTEM';
}
return 'UNKNOWN';
}
@@ -215,6 +238,8 @@ function qn_debug_mode_name($mode) {
case QN_MODE_ORM: return 'ORM';
case QN_MODE_API: return 'API';
case QN_MODE_APP: return 'APP';
+ case QN_MODE_AAA: return 'AAA';
+ case QN_MODE_NET: return 'NET';
}
return 'UNKNOWN';
}
@@ -504,12 +529,18 @@ public static function init() {
'route' => 'equal\route\Router',
'log' => 'equal\log\Logger',
'cron' => 'equal\cron\Scheduler',
- 'dispatch' => 'equal\dispatch\Dispatcher'
+ 'dispatch' => 'equal\dispatch\Dispatcher',
+ 'db' => 'equal\db\DBConnector'
]);
- // make sure mandatory dependencies are available (reporter requires context)
try {
+ // make mandatory dependencies available
$container->get(['report', 'context']);
+ // register ORM classes auto-loader
+ $om = $container->get('orm');
+ // init collections provider
+ $container->get('equal\orm\Collections');
+ spl_autoload_register([$om, 'getModel']);
}
catch(\Throwable $e) {
// fallback to a manual HTTP 500
@@ -522,17 +553,6 @@ public static function init() {
// and raise an exception (will be output in PHP error log)
throw new \Exception("missing_mandatory_dependency", QN_REPORT_FATAL);
}
- // register ORM classes autoloader
- try {
- $om = $container->get('orm');
- // init collections provider
- $container->get('equal\orm\Collections');
- spl_autoload_register([$om, 'getModel']);
- }
- catch(\Throwable $e) {
- throw new \Exception("autoload_register_failed", QN_REPORT_FATAL);
- }
-
}
public static function getLastContext() {
@@ -586,7 +606,7 @@ public static function announce(array $announcement) {
// set Response default Content Type to JSON
$response->headers()->setContentType('application/json');
- $reporter->debug("method $method");
+ $reporter->debug("API::method: $method");
// normalize $announcement array
if(!isset($announcement['params'])) {
@@ -722,14 +742,14 @@ public static function announce(array $announcement) {
$expires = intval($announcement['response']['expires']);
$age = time() - filemtime(realpath($cache_filename));
if($age >= $expires) {
- $reporter->debug("expired cache-id {$cache_id}");
+ $reporter->debug("API::expired cache-id {$cache_id}");
$serve_from_cache = false;
}
}
// handle manual request for invalidating the cache
if(isset($body['cache'])) {
if(in_array($body['cache'], [null, false, 0, '0'])) {
- $reporter->debug("manual reset cache-id {$cache_id}");
+ $reporter->debug("API::manual reset cache-id {$cache_id}");
$serve_from_cache = false;
}
// cache is a reserved parameter: no further process
@@ -739,7 +759,7 @@ public static function announce(array $announcement) {
if(file_exists($cache_filename)) {
// cache was invalidated: remove related file
if(!$serve_from_cache) {
- $reporter->debug("invalidating cache-id {$cache_id}");
+ $reporter->debug("API::invalidating cache-id {$cache_id}");
unlink($cache_filename);
}
// cache is still valid: serve from cache
@@ -752,7 +772,7 @@ public static function announce(array $announcement) {
->send();
throw new \Exception('', 0);
}
- $reporter->debug("serving from cache-id {$cache_id}");
+ $reporter->debug("API::serving from cache-id {$cache_id}");
list($headers, $result) = unserialize(file_get_contents($cache_filename));
// build response with cached headers
foreach($headers as $header => $value) {
@@ -902,7 +922,7 @@ public static function announce(array $announcement) {
$allowed_params = array_keys($announcement['params']);
$unknown_params = array_diff(array_keys($body), $allowed_params);
foreach($unknown_params as $unknown_param) {
- $reporter->debug("dropped unexpected parameter '{$unknown_param}'");
+ $reporter->debug("API::dropped unexpected parameter '{$unknown_param}'");
unset($body[$unknown_param]);
}
$missing_params = array_diff($allowed_params, array_intersect($allowed_params, array_keys($body)));
@@ -941,11 +961,11 @@ public static function announce(array $announcement) {
}
else {
if(isset($config['default'])) {
- $reporter->warning("invalid value for non-mandatory parameter '{$param}' reverted to default '{$config['default']}'");
+ $reporter->warning("API::invalid value for non-mandatory parameter '{$param}' reverted to default '{$config['default']}'");
$result[$param] = $config['default'];
}
else {
- $reporter->warning("dropped invalid non-mandatory parameter '{$param}'");
+ $reporter->warning("API::dropped invalid non-mandatory parameter '{$param}'");
}
}
}
@@ -974,12 +994,12 @@ public static function announce(array $announcement) {
if(!in_array($param, $mandatory_params)) {
// if it has a default value, assign to it
if(isset($config['default'])) {
- $reporter->warning("invalid value {$value} for non-mandatory parameter '{$param}' reverted to default '{$config['default']}'");
+ $reporter->warning("API::invalid value {$value} for non-mandatory parameter '{$param}' reverted to default '{$config['default']}'");
$result[$param] = $config['default'];
}
else {
// otherwise, drop it
- $reporter->warning("dropped invalid non-mandatory parameter '{$param}'");
+ $reporter->warning("API::dropped invalid non-mandatory parameter '{$param}'");
unset($result[$param]);
}
}
@@ -990,7 +1010,7 @@ public static function announce(array $announcement) {
}
// report received parameters
- $reporter->debug("params: ".json_encode($result));
+ $reporter->debug("API::params: ".json_encode($result));
if(count($invalid_params)) {
// no feedback about services
@@ -1059,8 +1079,13 @@ public static function announce(array $announcement) {
* @example run('get', 'model_read', ['entity' => 'core\Group', 'id'=> 1]);
*/
public static function run($type, $operation, $body=[], $root=false) {
- trigger_error("PHP::calling run method for $type:$operation", QN_REPORT_DEBUG);
global $last_context;
+ /** @var \equal\services\Container */
+ $container = Container::getInstance();
+ /** @var \equal\error\Reporter */
+ $reporter = $container->get('report');
+
+ $reporter->info("API::operation: $type:$operation");
$result = '';
$resolved = [
@@ -1076,7 +1101,6 @@ public static function run($type, $operation, $body=[], $root=false) {
'get' => ['kind' => 'DATA_PROVIDER', 'dir' => 'data' ], // return some data
'show' => ['kind' => 'APPLICATION', 'dir' => 'apps' ] // output rendering information (UI)
];
- $container = Container::getInstance();
if(!$root) {
$context_orig = $container->get('context');
@@ -1093,9 +1117,6 @@ public static function run($type, $operation, $body=[], $root=false) {
$context = $container->get('context');
}
- /** @var \equal\error\Reporter */
- $reporter = $container->get('report');
-
$getOperationOutput = function($script) use($context) {
ob_start();
try {
@@ -1133,6 +1154,12 @@ public static function run($type, $operation, $body=[], $root=false) {
$request = $context->httpRequest();
$request->body($body);
+ $reporter->info("API::".json_encode([
+ 'uri' => (string) $request->getUri(),
+ 'headers' => $request->getHeaders(true),
+ 'body' => $request->getBody()
+ ], JSON_PRETTY_PRINT));
+
$operation = explode(':', $operation);
if(count($operation) > 1) {
$visibility = array_shift($operation);
@@ -1151,12 +1178,6 @@ public static function run($type, $operation, $body=[], $root=false) {
}
}
- $reporter->debug(json_encode([
- 'uri' => (string) $request->getUri(),
- 'headers' => $request->getHeaders(true),
- 'body' => $request->getBody()
- ], JSON_PRETTY_PRINT));
-
// load package custom configuration, if any
if(!is_null($resolved['package']) && is_file(QN_BASEDIR.'/packages/'.$resolved['package'].'/config.json')) {
$data = file_get_contents(QN_BASEDIR.'/packages/'.$resolved['package'].'/config.json');
@@ -1233,10 +1254,10 @@ public static function run($type, $operation, $body=[], $root=false) {
$context->httpResponse()->header('Etag', $cache_id);
$headers = $context->httpResponse()->headers()->toArray();
file_put_contents(QN_BASEDIR.'/cache/'.$cache_id, serialize([$headers, $result]));
- $reporter->debug("stored cache-id {$cache_id}");
+ $reporter->debug("API::stored cache-id {$cache_id}");
}
}
- trigger_error("PHP::result: $result", QN_REPORT_DEBUG);
+ trigger_error("API::result: $result", QN_REPORT_DEBUG);
}
// restore context
@@ -1289,7 +1310,7 @@ public static function load_class($class_name) {
$result = include_once $file_path.'.class.php';
}
// Fallback to simple php extension
- else if(file_exists($file_path.'.php')) {
+ elseif(file_exists($file_path.'.php')) {
$result = include_once $file_path.'.php';
}
else {
@@ -1303,7 +1324,7 @@ public static function load_class($class_name) {
}
- // Initialize the eQual class for further 'load_class' calls
+ // bootstrap eQual
eQual::init();
}
namespace {
diff --git a/lib/equal/db/DBConnection.class.php b/lib/equal/db/DBConnection.class.php
index 04a7de2b5..561d9ff6d 100644
--- a/lib/equal/db/DBConnection.class.php
+++ b/lib/equal/db/DBConnection.class.php
@@ -1,58 +1,32 @@
- Some Rights Reserved, Cedric Francoys, 2010-2021
- Licensed under GNU LGPL 3 license
+ This file is part of the eQual framework
+ Some Rights Reserved, eQual framework, 2010-2024
+ Original author(s): Cédric FRANCOYS
+ License: GNU LGPL 3 license
*/
namespace equal\db;
-use equal\organic\Service;
/**
- * Service implementing factory pattern for DBManipulator instances.
+ * This class uses factory pattern for providing DBManipulator instances.
*/
-class DBConnection extends Service {
+class DBConnection {
- /**
- * @var DBManipulator
- */
- private $dbConnection;
+ public static function create(string $dbms='', string $host='', int $port=null, string $name='', string $user='', string $password='', string $charset='', string $collation='') {
+ /** @var DBManipulator */
+ $dbConnection = null;
- protected function __construct() {
- switch(constant('DB_DBMS')) {
+ switch($dbms) {
case 'MARIADB':
case 'MYSQL' :
- $this->dbConnection = new DBManipulatorMySQL(
- constant('DB_HOST'),
- constant('DB_PORT'),
- constant('DB_NAME'),
- constant('DB_USER'),
- constant('DB_PASSWORD'),
- constant('DB_CHARSET'),
- constant('DB_COLLATION')
- );
+ $dbConnection = new DBManipulatorMySQL($host, $port, $name, $user, $password, $charset, $collation);
break;
case 'SQLSRV' :
- $this->dbConnection = new DBManipulatorSqlSrv(
- constant('DB_HOST'),
- constant('DB_PORT'),
- constant('DB_NAME'),
- constant('DB_USER'),
- constant('DB_PASSWORD'),
- constant('DB_CHARSET'),
- constant('DB_COLLATION')
- );
+ $dbConnection = new DBManipulatorSqlSrv($host, $port, $name, $user, $password, $charset, $collation);
break;
case 'SQLITE' :
- $this->dbConnection = new DBManipulatorSQLite(
- constant('DB_HOST'),
- constant('DB_PORT'),
- constant('DB_NAME'),
- constant('DB_USER'),
- constant('DB_PASSWORD'),
- constant('DB_CHARSET'),
- constant('DB_COLLATION')
- );
+ $dbConnection = new DBManipulatorSQLite($host, $port, $name, $user, $password, $charset, $collation);
break;
case 'POSTGRESQL' :
// #todo
@@ -60,80 +34,8 @@ protected function __construct() {
case 'ORACLE' :
// #todo
break;
- default:
- $this->dbConnection = null;
}
-
- if(defined('DB_REPLICATION') && constant('DB_REPLICATION') != 'NO') {
- // add replica members, if any
- $i = 1;
-
- while(defined('DB_'.$i.'_HOST')
- && defined('DB_'.$i.'_PORT')
- && defined('DB_'.$i.'_USER')
- && defined('DB_'.$i.'_PASSWORD')
- && defined('DB_'.$i.'_NAME')) {
-
- $this->addReplicaMember(
- constant('DB_'.$i.'_HOST'),
- constant('DB_'.$i.'_PORT'),
- constant('DB_'.$i.'_NAME'),
- constant('DB_'.$i.'_USER'),
- constant('DB_'.$i.'_PASSWORD')
- );
- ++$i;
- }
- }
- }
-
- public static function constants() {
- return ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_DBMS', 'DB_CHARSET', 'DB_COLLATION'];
- }
-
- public function addReplicaMember($host, $port, $db, $user, $pass) {
- $member = null;
- switch(constant('DB_DBMS')) {
- case 'MYSQL' :
- $member = new DBManipulatorMySQL($host, $port, $db, $user, $pass);
- break;
- /*
- // insert handling of other DBMS here
- case 'XYZ' :
- $this->dbConnection = new DBManipulatorXyz(DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD);
- break;
- */
- default:
- break;
- }
- if($member && $this->dbConnection) {
- $this->dbConnection->addMember($member);
- }
- }
-
- public function connect($auto_select=true) {
- if(!isset($this->dbConnection)) return false;
- return $this->dbConnection->connect($auto_select);
- }
-
- public function disconnect() {
- if(!isset($this->dbConnection)) return true;
- return $this->dbConnection->disconnect();
- }
-
- /**
- * Magic overloading method: catch any call and relay it to DBConnection object
- *
- * @param string $name
- * @param array $arguments
- * @return mixed
- */
- public function __call($name, $arguments) {
-
- if (!$this->dbConnection) {
- return null;
- }
-
- return call_user_func_array([$this->dbConnection, $name], $arguments);
+ return $dbConnection;
}
}
diff --git a/lib/equal/db/DBConnector.class.php b/lib/equal/db/DBConnector.class.php
new file mode 100644
index 000000000..449835341
--- /dev/null
+++ b/lib/equal/db/DBConnector.class.php
@@ -0,0 +1,89 @@
+
+ Some Rights Reserved, eQual framework, 2010-2024
+ Original author(s): Cédric FRANCOYS
+ License: GNU LGPL 3 license
+*/
+namespace equal\db;
+
+use equal\organic\Service;
+
+/**
+ * Service for connecting to the DBMS holding the database of the current installation.
+ * This service acts as a facade for DB interactions.
+ */
+class DBConnector extends Service {
+
+ /** @var DBManipulator */
+ private $connection;
+
+ public static function constants() {
+ return ['DB_DBMS', 'DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_CHARSET', 'DB_COLLATION'];
+ }
+
+ protected function __construct() {
+ $this->connection = DBConnection::create(
+ constant('DB_DBMS'),
+ constant('DB_HOST'),
+ constant('DB_PORT'),
+ constant('DB_NAME'),
+ constant('DB_USER'),
+ constant('DB_PASSWORD'),
+ constant('DB_CHARSET'),
+ constant('DB_COLLATION')
+ );
+
+ if(defined('DB_REPLICATION') && constant('DB_REPLICATION') != 'NO') {
+ // add replica members, if any
+ $i = 1;
+
+ while(defined('DB_'.$i.'_HOST')
+ && defined('DB_'.$i.'_PORT')
+ && defined('DB_'.$i.'_USER')
+ && defined('DB_'.$i.'_PASSWORD')
+ && defined('DB_'.$i.'_NAME')) {
+
+ $this->addReplicaMember(
+ constant('DB_'.$i.'_HOST'),
+ constant('DB_'.$i.'_PORT'),
+ constant('DB_'.$i.'_NAME'),
+ constant('DB_'.$i.'_USER'),
+ constant('DB_'.$i.'_PASSWORD')
+ );
+ ++$i;
+ }
+ }
+ }
+
+ public function addReplicaMember($host, $port, $db, $user, $pass) {
+ /** @var DBManipulator */
+ $member = DBConnection::create(constant('DB_DBMS'), $host, $port, $db, $user, $pass);
+ if($member && $this->connection) {
+ $this->connection->addMember($member);
+ }
+ }
+
+ public function connect($auto_select=true) {
+ return isset($this->connection)?$this->connection->connect($auto_select):false;
+ }
+
+ public function disconnect() {
+ return !isset($this->connection) || $this->connection->disconnect();
+ }
+
+ /**
+ * Magic overloading method: catch any call and relay it to DBManipulator object
+ *
+ * @param string $name
+ * @param array $arguments
+ * @return mixed
+ */
+ public function __call($name, $arguments) {
+ if (!$this->connection) {
+ return null;
+ }
+ return call_user_func_array([$this->connection, $name], $arguments);
+ }
+
+}
diff --git a/lib/equal/db/DBManipulator.class.php b/lib/equal/db/DBManipulator.class.php
index 6c541c069..95870ff18 100644
--- a/lib/equal/db/DBManipulator.class.php
+++ b/lib/equal/db/DBManipulator.class.php
@@ -6,6 +6,9 @@
*/
namespace equal\db;
+/**
+ * This class is used as abstract class providing members and methods signature for DBManipulator class that extend it.
+ */
class DBManipulator {
/**
@@ -136,6 +139,7 @@ public function addMember(DBManipulator $member) {
* @access public
*/
public function connect($auto_select=true) {
+ return $this;
}
public function select($db_name) {
@@ -206,6 +210,14 @@ public function getLastQuery() {
return $this->last_query;
}
+ public static function fetchRow($result) {
+ return [];
+ }
+
+ public static function fetchArray($result) {
+ return [];
+ }
+
protected function setLastId($id) {
$this->last_id = $id;
}
@@ -218,4 +230,42 @@ protected function setLastQuery($query) {
$this->last_query = $query;
}
+ /**
+ * Get records from specified table, according to some conditions.
+ *
+ * @param array $tables name of involved tables
+ * @param array $fields list of requested fields
+ * @param array $ids ids to which the selection is limited
+ * @param array $conditions list of arrays (field, operand, value)
+ * @param string $id_field name of the id field ('id' by default)
+ * @param mixed $order string holding name of the order field or maps holding field names as keys and sorting as value
+ * @param integer $start
+ * @param integer $limit
+ *
+ * @return resource reference to query resource
+ */
+ public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $id_field='id', $order=[], $start=0, $limit=0) {}
+
+ public function setRecords($table, $ids, $fields, $conditions=null, $id_field='id') {}
+
+ /**
+ * Inserts new records in specified table.
+ *
+ * @param string $table name of the table in which insert the records
+ * @param array $fields list of involved fields
+ * @param array $values array of arrays specifying the values related to each specified field
+ * @return resource reference to query resource
+ */
+ public function addRecords($table, $fields, $values) {}
+
+ public function deleteRecords($table, $ids, $conditions=null, $id_field='id') {}
+
+ /**
+ * Fetch and increment the column of a series of records in a single operation.
+ * This method implements FAA instruction (fetch-and-add) in order to read and update a column as an atomic operation.
+ *
+ * @param int $increment A numeric value used to increment columns (if value positive) or decrement columns (if value is negative).
+ */
+ public function incRecords($table, $ids, $field, $increment, $id_field='id') {
+ }
}
diff --git a/lib/equal/db/DBManipulatorMySQL.class.php b/lib/equal/db/DBManipulatorMySQL.class.php
index 1546ccd93..b3e68d177 100644
--- a/lib/equal/db/DBManipulatorMySQL.class.php
+++ b/lib/equal/db/DBManipulatorMySQL.class.php
@@ -10,8 +10,7 @@
* DBManipulator implementation for MySQL server.
*
*/
-
-class DBManipulatorMySQL extends DBManipulator {
+final class DBManipulatorMySQL extends DBManipulator {
public static $types_associations = [
@@ -405,20 +404,6 @@ private function getConditionClause($id_field, $ids, $conditions) {
return $sql;
}
- /**
- * Get records from specified table, according to some conditions.
- *
- * @param array $tables name of involved tables
- * @param array $fields list of requested fields
- * @param array $ids ids to which the selection is limited
- * @param array $conditions list of arrays (field, operand, value)
- * @param string $id_field name of the id field ('id' by default)
- * @param mixed $order string holding name of the order field or maps holding field nmaes as keys and sorting as value
- * @param integer $start
- * @param integer $limit
- *
- * @return resource reference to query resource
- */
public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $id_field='id', $order=[], $start=0, $limit=0) {
// cast tables to an array (passing a single table is accepted)
if(!is_array($tables)) {
@@ -435,7 +420,7 @@ public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $
// test values and types
if(empty($tables)) {
- throw new \Exception(__METHOD__." : unable to build sql query, parameter 'tables' array is empty.", QN_ERROR_SQL);
+ throw new \Exception(__METHOD__." : unable to build sql query, parameter 'tables' is empty.", QN_ERROR_SQL);
}
/* irrelevant
if(!empty($fields) && !is_array($fields)) throw new \Exception(__METHOD__." : unable to build sql query, parameter 'fields' is not an array.", QN_ERROR_SQL);
@@ -519,14 +504,6 @@ public function setRecords($table, $ids, $fields, $conditions=null, $id_field='i
return $this->sendQuery($sql);
}
- /**
- * Inserts new records in specified table.
- *
- * @param string $table name of the table in which insert the records
- * @param array $fields list of involved fields
- * @param array $values array of arrays specifying the values related to each specified field
- * @return resource reference to query resource
- */
public function addRecords($table, $fields, $values) {
if (!is_array($fields) || !is_array($values)) {
throw new \Exception(__METHOD__.' : at least one parameter is missing', QN_ERROR_SQL);
@@ -543,4 +520,18 @@ public function deleteRecords($table, $ids, $conditions=null, $id_field='id') {
return $this->sendQuery($sql);
}
+ /**
+ * Fetch and increment the column of a series of records in a single operation.
+ *
+ * MySQL requires multi queries to support a single input with instructions separator.
+ * So we use LOCK TABLES and UNLOCK TABLES to make sure no change occurs between update and read.
+ */
+ public function incRecords($table, $ids, $field, $increment, $id_field='id') {
+ $res = null;
+ $this->sendQuery('LOCK TABLES `'.$table.'` WRITE;');
+ $this->sendQuery("UPDATE `{$table}` SET `{$field}` = `{$field}` + $increment WHERE `{$id_field}` in (".implode(',', $ids).");");
+ $res = $this->sendQuery("SELECT `{$id_field}`, `{$field}` FROM `{$table}` WHERE `{$id_field}` in (".implode(',', $ids).");");
+ $this->sendQuery('UNLOCK TABLES;');
+ return $res;
+ }
}
diff --git a/lib/equal/db/DBManipulatorSQLite.class.php b/lib/equal/db/DBManipulatorSQLite.class.php
index 8a0b32e44..0574cbf98 100644
--- a/lib/equal/db/DBManipulatorSQLite.class.php
+++ b/lib/equal/db/DBManipulatorSQLite.class.php
@@ -10,8 +10,7 @@
* DBManipulator implementation for MySQL server.
*
*/
-
-class DBManipulatorSQLite extends DBManipulator {
+final class DBManipulatorSQLite extends DBManipulator {
public static $types_associations = [
@@ -66,12 +65,14 @@ public function connect($auto_select=true) {
// by convention the DB file is the given DB_NAME with `.db` suffix
$db_file = QN_BASEDIR.'/bin/'.$this->db_name.'.db';
- $this->dbms_handler = new \SQLite3($db_file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
-
if(!file_exists($db_file)) {
return false;
}
+ $this->dbms_handler = new \SQLite3($db_file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
+ // make sure PHP process waits when an exclusive transaction is pending
+ $this->dbms_handler->busyTimeout(3000);
+
return $this;
}
@@ -417,20 +418,6 @@ private function getConditionClause($id_field, $ids, $conditions) {
return $sql;
}
- /**
- * Get records from specified table, according to some conditions.
- *
- * @param array $tables name of involved tables
- * @param array $fields list of requested fields
- * @param array $ids ids to which the selection is limited
- * @param array $conditions list of arrays (field, operand, value)
- * @param string $id_field name of the id field ('id' by default)
- * @param mixed $order string holding name of the order field or maps holding field names as keys and sorting as value
- * @param integer $start
- * @param integer $limit
- *
- * @return resource reference to query resource
- */
public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $id_field='id', $order=[], $start=0, $limit=0) {
// cast tables to an array (passing a single table is accepted)
if(!is_array($tables)) {
@@ -531,14 +518,6 @@ public function setRecords($table, $ids, $fields, $conditions=null, $id_field='i
return $this->sendQuery($sql);
}
- /**
- * Inserts new records in specified table.
- *
- * @param string $table name of the table in which insert the records
- * @param array $fields list of involved fields
- * @param array $values array of arrays specifying the values related to each specified field
- * @return resource reference to query resource
- */
public function addRecords($table, $fields, $values) {
if (!is_array($fields) || !is_array($values)) {
throw new \Exception(__METHOD__.' : at least one parameter is missing', QN_ERROR_SQL);
@@ -555,4 +534,16 @@ public function deleteRecords($table, $ids, $conditions=null, $id_field='id') {
return $this->sendQuery($sql);
}
+ /**
+ * Fetch and increment the column of a series of records in a single operation.
+ *
+ */
+ public function incRecords($table, $ids, $field, $increment, $id_field='id') {
+ $sql = 'BEGIN EXCLUSIVE TRANSACTION;';
+ $sql .= "UPDATE `{$table}` SET `{$field}` = `{$field}` + $increment WHERE `{$id_field}` in (".implode(',', $ids).");";
+ $sql .= 'COMMIT TRANSACTION;';
+ $sql .= "SELECT `{$id_field}`, `{$field}` FROM `{$table}` WHERE `{$id_field}` in (".implode(',', $ids).");";
+ return $this->sendQuery($sql);
+ }
+
}
diff --git a/lib/equal/db/DBManipulatorSqlSrv.class.php b/lib/equal/db/DBManipulatorSqlSrv.class.php
index 113d2c4f1..c859ec3cf 100644
--- a/lib/equal/db/DBManipulatorSqlSrv.class.php
+++ b/lib/equal/db/DBManipulatorSqlSrv.class.php
@@ -10,8 +10,7 @@
* DBManipulator implementation for MS SQL server.
*
*/
-
-class DBManipulatorSqlSrv extends DBManipulator {
+final class DBManipulatorSqlSrv extends DBManipulator {
/*
@@ -289,7 +288,7 @@ function sendQuery($query, $sql_operation='') {
foreach($this->members as $member) {
$member->sendQuery($query);
}
- if($sql_operation =='insert') {
+ if($sql_operation == 'insert') {
if($row = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)) {
$this->setLastId($row['id']);
}
@@ -330,13 +329,13 @@ private function escapeString($value) {
if(gettype($value) == 'string' && strlen($value) == 0) {
$result = "''";
}
- else if(in_array(gettype($value), ['integer', 'double'])) {
+ elseif(in_array(gettype($value), ['integer', 'double'])) {
$result = $value;
}
- else if(gettype($value) == 'boolean') {
+ elseif(gettype($value) == 'boolean') {
$result = ($value)?'1':'0';
}
- else if(is_null($value)) {
+ elseif(is_null($value)) {
$result = 'NULL';
}
else {
@@ -426,20 +425,6 @@ private function getConditionClause($id_field, $ids, $conditions) {
return $sql;
}
- /**
- * Get records from specified table, according to some conditions.
- *
- * @param array $tables name of involved tables
- * @param array $fields list of requested fields
- * @param array $ids ids to which the selection is limited
- * @param array $conditions list of arrays (field, operand, value)
- * @param string $id_field name of the id field ('id' by default)
- * @param mixed $order string holding name of the order field or maps holding field nmaes as keys and sorting as value
- * @param integer $start
- * @param integer $limit
- *
- * @return resource reference to query resource
- */
public function getRecords($tables, $fields=NULL, $ids=NULL, $conditions=NULL, $id_field='id', $order=[], $start=0, $limit=0) {
// cast tables to an array (passing a single table is accepted)
$tables = (array) $tables;
@@ -526,15 +511,6 @@ public function setRecords($table, $ids, $fields, $conditions=null, $id_field='i
return $this->sendQuery($sql, 'update');
}
-
- /**
- * Inserts new records in specified table.
- *
- * @param string $table name of the table in which insert the records
- * @param array $fields list of involved fields
- * @param array $values array of arrays specifying the values related to each specified field
- * @return resource reference to query resource
- */
public function addRecords($table, $fields, $values) {
if (!is_array($fields) || !is_array($values)) {
throw new \Exception(__METHOD__.' : at least one parameter is missing', QN_ERROR_SQL);
@@ -551,4 +527,18 @@ public function deleteRecords($table, $ids, $conditions=null, $id_field='id') {
return $this->sendQuery($sql, 'delete');
}
+ /**
+ * Fetch and increment the column of a series of records in a single operation.
+ *
+ * For unknown reason, if the select is done after the update, no result set is returned.
+ * That is why we compute the expected result in the first select statement, marked with TABLOCKX to make sure the server locks the table before updating it.
+ */
+ public function incRecords($table, $ids, $field, $increment, $id_field='id') {
+ $sql = 'BEGIN TRANSACTION;';
+ $sql .= "SELECT [{$id_field}], ([{$field}] + $increment) as $field FROM [{$table}] WITH (TABLOCKX) WHERE [{$id_field}] in (".implode(',', $ids).");";
+ $sql .= "UPDATE [{$table}] SET [{$field}] = [{$field}] + $increment WHERE [{$id_field}] in (".implode(',', $ids).");";
+ $sql .= 'COMMIT;';
+ return $this->sendQuery($sql, 'update');
+ }
+
}
diff --git a/lib/equal/error/Reporter.class.php b/lib/equal/error/Reporter.class.php
index a49f78567..629beb1c2 100644
--- a/lib/equal/error/Reporter.class.php
+++ b/lib/equal/error/Reporter.class.php
@@ -15,16 +15,25 @@ class Reporter extends Service {
private $thread_id;
+ /**
+ * Static list of constants required by current provider
+ *
+ */
+ public static function constants() {
+ return ['DEBUG_MODE', 'DEBUG_LEVEL', 'QN_LOG_STORAGE_DIR', 'QN_REPORT_SYSTEM', 'QN_REPORT_FATAL', 'QN_REPORT_ERROR', 'QN_REPORT_WARNING', 'QN_REPORT_DEBUG', 'QN_REPORT_INFO', 'QN_REPORT_DEBUG'];
+ }
+
/**
* Constructor defines which methods have to be called when errors and uncaught exceptions occur
*
+ * Note: $thread_id depends on the current PHP thread.
+ * A same thread can stack several contexts. In the console, logs are grouped based on their thread_id.
*/
public function __construct() {
- // #memo - $thread_id depends on the current PHP thread. A same thread can stack several contexts. In the console, logs are grouped based on their thread_id.
$this->thread_id = substr(md5(getmypid().';'.hrtime(true)), 0, 8);
$this->debug_mode = (defined('DEBUG_MODE'))?constant('DEBUG_MODE'):0;
$this->debug_level = (defined('DEBUG_LEVEL'))?constant('DEBUG_LEVEL'):0;
- // ::errorHandler() will deal with error and debug messages depending on debug source value
+ // ::errorHandler() will deal with errors and debug messages depending on debug source value
ini_set('display_errors', 1);
// use QN_REPORT_x for reporting, E_ERROR for fatal errors only, E_ALL for all errors
error_reporting($this->debug_level);
@@ -33,16 +42,8 @@ public function __construct() {
}
/**
- * Static list of constants required by current provider
- *
- */
- public static function constants() {
- return ['QN_LOG_STORAGE_DIR', 'QN_REPORT_FATAL', 'QN_REPORT_ERROR', 'QN_REPORT_WARNING', 'QN_REPORT_DEBUG', 'QN_REPORT_INFO', 'QN_REPORT_DEBUG'];
- }
-
- /**
- * Handles uncaught exceptions, which include deliberately triggered fatal-error
- * In all cases, these are critical errors that cannot be recovered and need an immediate stop (fatal error)
+ * Handles uncaught exceptions, which include deliberately triggered fatal-error.
+ * Uncaught errors imply a critical issue that cannot be recovered and need an immediate stop (fatal error).
*/
public static function uncaughtExceptionHandler($exception) {
self::handleThrowable($exception);
@@ -57,6 +58,8 @@ public static function handleThrowable($exception) {
$backtrace = $exception->getTrace();
if(count($backtrace)) {
$trace = array_shift($backtrace);
+ $trace['file'] = $exception->getFile();
+ $trace['line'] = $exception->getLine();
$trace['stack'] = $backtrace;
$instance->log(QN_REPORT_ERROR, $msg, $trace);
}
@@ -73,8 +76,8 @@ public static function handleThrowable($exception) {
* @param mixed $errcontext
*/
public static function errorHandler($errno, $errmsg, $errfile='', $errline=0, $errcontext=[]) {
- // dismiss handler if not required
- if (!(error_reporting() & $errno)) {
+ // dismiss processing if not required
+ if ($errno > 0 && !(error_reporting() & $errno)) {
return;
}
// adapt error code
@@ -83,12 +86,12 @@ public static function errorHandler($errno, $errmsg, $errfile='', $errline=0, $e
$depth = 0;
switch($errno) {
// handler was invoked using trigger_error()
- case QN_REPORT_DEBUG: // E_USER_DEPRECATED
- case QN_REPORT_INFO: // E_USER_NOTICE
- case QN_REPORT_WARNING: // E_USER_WARNING
- case QN_REPORT_ERROR: // E_USER_ERROR
- // #memo - fatal errors always stop the script before reaching this point
- case QN_REPORT_FATAL: // E_ERROR
+ case EQ_REPORT_DEBUG: // E_USER_DEPRECATED
+ case EQ_REPORT_INFO: // E_USER_NOTICE
+ case EQ_REPORT_WARNING: // E_USER_WARNING
+ case EQ_REPORT_ERROR: // E_USER_ERROR
+ case EQ_REPORT_FATAL: // E_ERROR
+ case EQ_REPORT_SYSTEM: // 0
$depth = 2;
break;
// handler was invoked by PHP internals
@@ -103,10 +106,12 @@ public static function errorHandler($errno, $errmsg, $errfile='', $errline=0, $e
case E_WARNING:
case E_CORE_WARNING:
case E_COMPILE_WARNING:
+ $code = QN_REPORT_WARNING;
+ break;
case E_NOTICE:
case E_STRICT:
case E_DEPRECATED:
- $code = QN_REPORT_WARNING;
+ $code = QN_REPORT_INFO;
break;
}
// retrieve instance and log error
@@ -119,18 +124,17 @@ public static function errorHandler($errno, $errmsg, $errfile='', $errline=0, $e
* Appends one line to the log file.
*/
private function log($code, $msg, $trace) {
- // discard non-applicable log requests
- if($this->debug_mode == 0 || $this->debug_level == 0 || !($code & $this->debug_level)) {
+ // discard non-applicable log requests, with exception for $code = 0 (system message that must always be logged)
+ if($code > 0 && ($this->debug_mode == 0 || $this->debug_level == 0 || !($code & $this->debug_level))) {
return;
}
- // check reporting mode, if provided
- $mode = QN_MODE_PHP;
+ // retrieve reporting mode, if provided
+ $mode = EQ_MODE_PHP;
if(strpos($msg, '::') == 3) {
- // default to mask QN_MODE_PHP
- $source = QN_MODE_PHP;
+ $source = $mode;
$parts = explode('::', $msg, 2);
if($parts && count($parts) > 1) {
- $source = (strlen($parts[0]))?('QN_MODE_'.$parts[0]):$source;
+ $source = (strlen($parts[0]))?('EQ_MODE_'.$parts[0]):$source;
$msg = $parts[1];
}
if(!is_numeric($source) && @constant($source)) {
@@ -139,8 +143,12 @@ private function log($code, $msg, $trace) {
$mode = (int) $source;
}
// discard non-applicable log requests
- if(!($this->debug_mode & $mode)) {
- return;
+ // #memo - SYSTEM are always logged (code == 0)
+ if($code > 0) {
+ // discard if mode is not marked in debug_mode
+ if(!($this->debug_mode & $mode)) {
+ return;
+ }
}
$time_parts = explode(" ", microtime());
@@ -152,7 +160,7 @@ private function log($code, $msg, $trace) {
'level' => qn_debug_code_name($code),
'mode' => qn_debug_mode_name($mode),
'class' => (isset($trace['class']))?$trace['class']:'',
- 'function' => (isset($trace['function']))?$trace['function']:'',
+ 'function' => (isset($trace['function']))?(strlen($trace['function'])?$trace['function'].'()':'[main]'):'',
'file' => (isset($trace['file']))?$trace['file']:'',
'line' => (isset($trace['line']))?$trace['line']:'',
'message' => $msg,
@@ -223,24 +231,24 @@ private static function getTrace($offset=0) {
}
public function fatal($msg) {
- $this->log(QN_REPORT_FATAL, $msg, self::getTrace(2));
+ $this->log(QN_REPORT_FATAL, $msg, self::getTrace(1));
die('fatal_error');
}
public function error($msg) {
- $this->log(QN_REPORT_ERROR, $msg, self::getTrace(2));
+ $this->log(QN_REPORT_ERROR, $msg, self::getTrace(1));
}
public function warning($msg) {
- $this->log(QN_REPORT_WARNING, $msg, self::getTrace(2));
+ $this->log(QN_REPORT_WARNING, $msg, self::getTrace(1));
}
public function info($msg) {
- $this->log(QN_REPORT_INFO, $msg, self::getTrace(2));
+ $this->log(QN_REPORT_INFO, $msg, self::getTrace(1));
}
public function debug($msg) {
- $this->log(QN_REPORT_DEBUG, $msg, self::getTrace(2));
+ $this->log(QN_REPORT_DEBUG, $msg, self::getTrace(1));
}
}
diff --git a/lib/equal/http/HttpResponse.class.php b/lib/equal/http/HttpResponse.class.php
index b318e572a..57f9b740c 100644
--- a/lib/equal/http/HttpResponse.class.php
+++ b/lib/equal/http/HttpResponse.class.php
@@ -99,7 +99,7 @@ public function send() {
case 'application/vnd.api+json':
case 'application/x-json':
case 'application/json':
- $body = json_encode($body, JSON_PRETTY_PRINT);
+ $body = json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if($body === false) {
throw new \Exception('invalid_json_input', QN_ERROR_UNKNOWN);
}
diff --git a/lib/equal/organic/Service.class.php b/lib/equal/organic/Service.class.php
index 9b847e99f..8e2259e79 100644
--- a/lib/equal/organic/Service.class.php
+++ b/lib/equal/organic/Service.class.php
@@ -8,7 +8,7 @@
use equal\services\Container;
class Service extends Singleton {
- /* All services are instanciated throught the service container
+ /* All services are instantiated through the service container
which instance is, in turn, made available as public member
*/
public $container;
diff --git a/lib/equal/orm/Collection.class.php b/lib/equal/orm/Collection.class.php
index dd58b14f0..a70971cf8 100644
--- a/lib/equal/orm/Collection.class.php
+++ b/lib/equal/orm/Collection.class.php
@@ -386,14 +386,14 @@ public function lang($lang) {
* @throws Exception if some value could not be validated against class constraints (see {class}::getConstraints method)
*/
private function validate(array $fields, $ids=[], $check_unique=false, $check_required=false) {
- $validation = $this->orm->validate($this->class, $ids, $fields, $check_unique, $check_required);
- if($validation < 0 || count($validation)) {
- foreach($validation as $error_code => $error_descr) {
- if(is_array($error_descr)) {
- $error_descr = serialize($error_descr);
+ $errors = $this->orm->validate($this->class, $ids, $fields, $check_unique, $check_required);
+ if(count($errors)) {
+ foreach($errors as $error_id => $description) {
+ if(is_array($description)) {
+ $description = serialize($description);
}
// send error using the same format as the announce method
- throw new \Exception($error_descr, (int) $error_code);
+ throw new \Exception($description, (int) $error_id);
}
}
}
@@ -665,10 +665,10 @@ public function read($fields, $lang=null) {
$requested_fields = [];
// 'id': we might access an object directly by giving its `id`.
- // 'state': the state of the object is provided for concurrency control (check that a draft object is not validated twice).
- // 'deleted': since some objects might have been soft-deleted we need to load the `deleted` state in order to know if object needs to be in the result set or not.
- // 'modified': the last update timestamp is always provided. At update, if modified is provided, it is compared to the current timestamp to detect concurrent changes.
- // #memo - we cannot add 'name' by default since it might be a computed field (that could lead to a circular dependency)
+ // 'state': the state of the object is provided for concurrency control (check that a draft object is not instantiated twice).
+ // 'deleted': since some objects might have been soft-deleted, we need to load the `deleted` state in order to know if object needs to be in the result set or not.
+ // 'modified': the last update timestamp is always provided. At update, if `modified` is provided, it is compared to the current timestamp to detect concurrent changes.
+ // #memo - we cannot add 'name' by default since it might be (an alias to) a computed field (that could lead to a circular dependency)
$mandatory_fields = ['id', /*'name',*/ 'state', 'deleted', 'modified'];
foreach($fields as $key => $val ) {
@@ -737,16 +737,19 @@ public function read($fields, $lang=null) {
if(is_numeric($field)) {
continue;
}
- // we accept single value as subfields
+ // accept both array or single value as subfields
$subfields = (array) $subfields;
// #memo - using Field object guarantees support for `alias` and `computed` fields
$targetField = $this->model->getField($field);
+ if(!$targetField) {
+ continue;
+ }
$target = $targetField->getDescriptor();
- $target_type = (isset($target['result_type']))?$target['result_type']:$target['type'];
- if(!in_array($target_type, ['one2many', 'many2one', 'many2many'])) {
+ if(!in_array($target['result_type'], ['one2many', 'many2one', 'many2many'])) {
continue;
}
+
$children_ids = [];
foreach($this->objects as $object) {
foreach((array) $object[$field] as $oid) {
@@ -763,8 +766,8 @@ public function read($fields, $lang=null) {
foreach($this->objects as $id => $object) {
/** @var Collection */
$children = $target['foreign_object']::ids($this->objects[$id][$field])->read($subfields, ($lang)?$lang:$this->lang);
- if($target_type == 'many2one') {
- // #memo - result might be null or an Object (that might contain sub-collections)
+ if($target['result_type'] == 'many2one') {
+ // #memo - result might be either null or a Model object (which might contain sub-collections)
$this->objects[$id][$field] = $children->first();
}
else {
diff --git a/lib/equal/orm/Field.class.php b/lib/equal/orm/Field.class.php
index ef1d930d7..94dbb3b58 100644
--- a/lib/equal/orm/Field.class.php
+++ b/lib/equal/orm/Field.class.php
@@ -12,7 +12,9 @@
class Field {
/**
- * Descriptor of the field, as returned by Model::getColumns()
+ * Descriptor of the field.
+ * In addition to properties from `Model::getColumns()`, `Field::descriptor` always as a `result_type` property.
+ *
* @var array
*/
private $descriptor = [];
@@ -30,11 +32,14 @@ class Field {
* @param array $descriptor Associative array mapping field properties and their values.
*/
public function __construct(array $descriptor) {
- // store original descriptor
- $this->descriptor = $descriptor;
if(isset($descriptor['type'])) {
$this->type = $descriptor['type'];
}
+ $this->descriptor = $descriptor;
+ // ensure local descriptor always has a result_type property
+ if(!isset($descriptor['result_type'])) {
+ $this->descriptor['result_type'] = $this->type;
+ }
}
/**
@@ -82,7 +87,35 @@ public function getUsage(): Usage {
* @return array
*/
public function getConstraints(): array {
- return $this->getUsage()->getConstraints();
+ // generate constraint based on type
+ $result_type = $this->descriptor['result_type'];
+
+ $constraints = [
+ 'invalid_type' => [
+ 'message' => "Value is not of type {$result_type}.",
+ 'function' => function($value) use($result_type) {
+ static $map = [
+ 'bool' => 'boolean',
+ 'int' => 'integer',
+ 'float' => 'double',
+ 'text' => 'string',
+ 'date' => 'integer',
+ 'datetime' => 'integer',
+ 'file' => 'string',
+ 'binary' => 'string',
+ 'many2one' => 'integer',
+ 'one2many' => 'array',
+ 'many2many' => 'array'
+ ];
+ // fix types to match values returned by PHP `gettype()`
+ $mapped_type = $map[$result_type] ?? $result_type;
+ return (gettype($value) == $mapped_type);
+ }
+ ]
+ ];
+
+ // append constraints based on usage
+ return array_merge($constraints, $this->getUsage()->getConstraints());
}
public function getDescriptor(): array {
diff --git a/lib/equal/orm/ObjectManager.class.php b/lib/equal/orm/ObjectManager.class.php
index 73b38b892..2888c315f 100644
--- a/lib/equal/orm/ObjectManager.class.php
+++ b/lib/equal/orm/ObjectManager.class.php
@@ -7,7 +7,7 @@
namespace equal\orm;
use equal\organic\Service;
-use equal\db\DBConnection;
+use equal\db\DBConnector;
use equal\data\DataValidator;
use \Exception as Exception;
@@ -62,8 +62,8 @@ class ObjectManager extends Service {
private $last_error;
/**
- * Instance to a DBConnection object
- * @var DBConnection
+ * DB connector instance
+ * @var DBConnector
*/
private $db;
@@ -71,22 +71,25 @@ class ObjectManager extends Service {
public static $simple_types = array('boolean', 'integer', 'float', 'string', 'text', 'date', 'time', 'datetime', 'file', 'binary', 'many2one');
public static $complex_types = array('one2many', 'many2many', 'computed');
+ /**
+ * - dev-2.0 - 'dependencies' has been deprecated in favor to 'dependents'
+ */
public static $valid_attributes = [
'alias' => array('description', 'help', 'type', 'visible', 'default', 'usage', 'alias', 'required', 'deprecated'),
- 'boolean' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'),
- 'integer' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'selection', 'unique'),
- 'float' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'selection', 'precision'),
- 'string' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang', 'selection', 'unique'),
- 'text' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang'),
- 'date' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'),
- 'time' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'),
- 'datetime' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'),
- 'file' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang'),
- 'binary' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang'),
- 'many2one' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'required', 'deprecated', 'foreign_object', 'domain', 'onupdate', 'ondelete', 'multilang'),
- 'one2many' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'deprecated', 'foreign_object', 'foreign_field', 'domain', 'onupdate', 'ondetach', 'order', 'sort'),
- 'many2many' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'deprecated', 'foreign_object', 'foreign_field', 'rel_table', 'rel_local_key', 'rel_foreign_key', 'domain', 'onupdate'),
- 'computed' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'readonly', 'deprecated', 'result_type', 'usage', 'function', 'onupdate', 'onrevert', 'store', 'instant', 'multilang', 'selection', 'foreign_object')
+ 'boolean' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'),
+ 'integer' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'selection', 'unique'),
+ 'float' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'selection', 'precision'),
+ 'string' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang', 'selection', 'unique'),
+ 'text' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang'),
+ 'date' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'),
+ 'time' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'),
+ 'datetime' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate'),
+ 'file' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang'),
+ 'binary' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'usage', 'required', 'deprecated', 'onupdate', 'multilang'),
+ 'many2one' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'required', 'deprecated', 'foreign_object', 'domain', 'onupdate', 'ondelete', 'multilang'),
+ 'one2many' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'deprecated', 'foreign_object', 'foreign_field', 'domain', 'onupdate', 'ondetach', 'order', 'sort'),
+ 'many2many' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'deprecated', 'foreign_object', 'foreign_field', 'rel_table', 'rel_local_key', 'rel_foreign_key', 'domain', 'onupdate'),
+ 'computed' => array('description', 'help', 'type', 'visible', 'default', 'dependencies', 'dependents', 'readonly', 'deprecated', 'result_type', 'usage', 'function', 'onupdate', 'onrevert', 'store', 'instant', 'multilang', 'selection', 'foreign_object')
];
public static $mandatory_attributes = [
@@ -170,9 +173,9 @@ class ObjectManager extends Service {
];
/**
- * @param DBConnection $db Instance of the Service allowing connection to DBMS (connection might not be established yet).
+ * @param DBConnector $db Instance of the Service allowing connection to DBMS (connection might not be established yet).
*/
- protected function __construct(DBConnection $db) {
+ protected function __construct(DBConnector $db) {
$this->db = &$db;
$this->packages = null;
$this->cache = [];
@@ -197,7 +200,7 @@ public static function constants() {
}
/**
- * #todo - deprecate : use the DBConnection service instead
+ * #todo - deprecate : use the DBConnector instead
* @deprecated
*/
public function getDB() {
@@ -205,10 +208,10 @@ public function getDB() {
}
/**
- * Provide the db handler (DBConnection instance).
+ * Provide the db handler (DBConnector instance).
* If the connection hasn't been established yet, tries to connect to DBMS.
*
- * @return DBConnection
+ * @return DBConnector
*/
private function getDBHandler() {
// open DB connection, if not connected yet
@@ -661,17 +664,26 @@ private function load($class, $ids, $fields, $lang) {
if(!ObjectManager::checkFieldAttributes(self::$mandatory_attributes, $schema, $field)) {
throw new Exception("missing at least one mandatory attribute for field '$field' of class '$class'", QN_ERROR_INVALID_PARAM);
}
- $res = $this->callonce($class, $schema[$field]['function'], $ids, [], $lang, ['ids', 'lang']);
- if($res > 0) {
- foreach($ids as $oid) {
- if(isset($res[$oid])) {
- // #memo - do not adapt : we're dealing with PHP not SQL
- $value = $res[$oid];
- }
- else {
- $value = null;
+ // spot the fields that have not been computed yet (or unset) during this cycle
+ $missing_ids = [];
+ foreach($ids as $oid) {
+ if(!isset($om->cache[$table_name][$oid][$lang][$field])) {
+ $missing_ids[] = $oid;
+ }
+ }
+ if(count($missing_ids)) {
+ $res = $this->callonce($class, $schema[$field]['function'], $missing_ids, [], $lang, ['ids', 'lang']);
+ if($res > 0) {
+ foreach($missing_ids as $oid) {
+ if(isset($res[$oid])) {
+ // #memo - do not adapt : we're dealing with PHP not SQL
+ $value = $res[$oid];
+ }
+ else {
+ $value = null;
+ }
+ $om->cache[$table_name][$oid][$lang][$field] = $value;
}
- $om->cache[$table_name][$oid][$lang][$field] = $value;
}
}
}
@@ -710,7 +722,9 @@ private function load($class, $ids, $fields, $lang) {
$fields_lists['simple'][] = $field;
}
}
- else $fields_lists[$type][] = $field;
+ else {
+ $fields_lists[$type][] = $field;
+ }
}
// 2) load fields values, grouping fields by type
@@ -862,7 +876,7 @@ private function store($class, $ids, $fields, $lang) {
$value = $om->cache[$table_name][$oid][$lang][$field];
if(!is_array($value)) {
if(is_numeric($value)) {
- $value = [intval($value)];
+ $value = (array) intval($value);
}
else {
trigger_error("ORM::wrong value for field '$field' of class '$class', should be an array", QN_REPORT_ERROR);
@@ -896,14 +910,14 @@ private function store($class, $ids, $fields, $lang) {
break;
case 'null':
default:
- $om->db->setRecords($foreign_table, $ids_to_remove, [ $schema[$field]['foreign_field'] => 0 ] );
+ $om->db->setRecords($foreign_table, $ids_to_remove, [ $schema[$field]['foreign_field'] => null ] );
break;
}
}
}
else {
// remove relation by setting pointing id to 0
- $om->db->setRecords($foreign_table, $ids_to_remove, [$schema[$field]['foreign_field'] => 0]);
+ $om->db->setRecords($foreign_table, $ids_to_remove, [$schema[$field]['foreign_field'] => null]);
}
}
// add relation by setting the pointing id (overwrite previous value if any)
@@ -1027,6 +1041,10 @@ private function store($class, $ids, $fields, $lang) {
*/
public function callonce($class, $method, $ids, $values=[], $lang=null, $signature=['ids', 'values', 'lang']) {
trigger_error("ORM::calling orm\ObjectManager::callonce {$class}::{$method}", QN_REPORT_DEBUG);
+
+ // stack current state of object_methods map (current state is restored at the end of the method)
+ $object_methods_state = $this->object_methods;
+
$result = [];
$lang = ($lang)?$lang:constant('DEFAULT_LANG');
@@ -1056,7 +1074,7 @@ public function callonce($class, $method, $ids, $values=[], $lang=null, $signatu
$this->object_methods[$called_class][$called_method] = [];
}
- // prevent inner loops and several calls to same handler with identical ids during the cycle (subsequent update() calls)
+ // prevent inner loops (several calls to same handler with identical ids) during the cycle (subsequent `callonce()` calls)
$processed_ids = $this->object_methods[$called_class][$called_method];
$unprocessed_ids = array_diff((array) $ids, $processed_ids);
$this->object_methods[$called_class][$called_method] = array_merge($processed_ids, $unprocessed_ids);
@@ -1101,6 +1119,9 @@ public function callonce($class, $method, $ids, $values=[], $lang=null, $signatu
$result = $e->getCode();
}
+ // unstack global object_methods state
+ $this->object_methods = $object_methods_state;
+
return $result;
}
@@ -1179,6 +1200,7 @@ public function call($class, $method, $ids, $values=[], $lang=null, $signature=[
/**
* Retrieve the static instance of a given class (Model with default values).
+ * This method is registered as autoload handler in `eq.lib.php`.
*
* @return boolean|Object Returns the static instance of the model with default values. If no Model matches the class name returns false.
*/
@@ -1189,8 +1211,7 @@ public function getModel($class) {
}
catch(Exception $e) {
trigger_error($e->getMessage(), QN_REPORT_ERROR);
- // #todo - validate (this is the only public method in ORM that raises an Exception)
- throw new Exception("unknown class '{$class}'", QN_ERROR_UNKNOWN_OBJECT);
+ // #memo - another autoload handler might be registered, so we relay without raising an exception
}
return $model;
}
@@ -1202,6 +1223,7 @@ public function getLastError() {
/**
* Checks whether a set of values is valid according to given class definition.
* This is done using the class validation method.
+ * Relations consistency check (verifying that targeted object(s) actually exist) is not performed here.
*
* Result example:
* "INVALID_PARAM": {
@@ -1213,16 +1235,16 @@ public function getLastError() {
* @param string $class Entity name.
* @param array $ids Array of objects identifiers.
* @param array $values Associative array mapping fields names with values to be assigned to the object(s).
- * @param boolean $check_unique Request check for unicity constraints (related to getUnique method).
+ * @param boolean $check_unique Request check for unique constraints (related to getUnique method).
* @param boolean $check_required Request check for required fields (and _self constraints).
+ *
* @return array Returns an associative array containing invalid fields with their associated error_message_id.
* An empty array means all fields are valid. In case of error, the method returns a negative integer.
*/
public function validate($class, $ids, $values, $check_unique=false, $check_required=false) {
- // #todo : check relational fields consistency (make sure that target object(s) actually exist)
-
$res = [];
+ /** @var \equal\orm\Model */
$model = $this->getStaticInstance($class);
$schema = $model->getSchema();
@@ -1248,109 +1270,54 @@ public function validate($class, $ids, $values, $check_unique=false, $check_requ
* 2) MODEL constraint check
*/
-
- // // #todo
- // // check constraints implied by type and usage
- // foreach($values as $field => $value) {
- // /** @var \equal\orm\Field */
- // $f = $model->getField($class);
- // $usage = $f->getUsage();
- // $constraints = $f->getConstraints();
- // foreach($constraints as $error_id => $constraint) {
- // if(!isset($constraint['function'])) {
- // continue;
- // }
- // $fn = $constraint['function'];
- // if(is_callable($fn)) {
- // $fn->bindTo($usage);
- // if(!call_user_func($fn, $value)) {
- // $res[$field][$error_id] = $constraint['message'];
- // }
- // }
- // }
- // }
-
- // // + support $model->getConstraints()
-
-
// get constraints defined in the model (schema)
$constraints = $model->getConstraints();
-
+ // append constraints implied by type and usage
foreach($values as $field => $value) {
- // add constraints based on field type : check that given value is not bigger than related DBMS column capacity
- if(isset($schema[$field]['type'])) {
- $type = $schema[$field]['type'];
- // adapt type based on usage
- if(isset($schema[$field]['usage'])) {
- switch($schema[$field]['usage']) {
- // #todo - continue this list
- case 'text':
- case 'text/plain':
- case 'text/json':
- case 'text/html':
- case 'text/json':
- $type = 'text';
- break;
- }
+ $f = $model->getField($field);
+ foreach($f->getConstraints() as $error_id => $constraint) {
+ if(!isset($constraint['function'])) {
+ continue;
}
- switch($type) {
- case 'string':
- $constraints[$field]['size_overflow'] = [
- 'message' => 'String length must be maximum 255 chars.',
- 'function' => function($val, $obj) {return (strlen($val) <= 255);}
- ];
- break;
- case 'text':
- $constraints[$field]['size_overflow'] = [
- 'message' => 'String length must be maximum 65,535 chars.',
- 'function' => function($val, $obj) {return (strlen($val) <= 65535);}
- ];
- break;
- case 'file':
- case 'binary':
- $constraints[$field]['size_overflow'] = [
- 'message' => 'String length must be maximum '.constant('UPLOAD_MAX_FILE_SIZE').' chars.',
- 'function' => function($val, $obj) {return (strlen($val) <= constant('UPLOAD_MAX_FILE_SIZE'));}
+ if(is_callable($constraint['function'])) {
+ $closure = \Closure::fromCallable($constraint['function']);
+ $closure->bindTo($f->getUsage());
+ if(!isset($constraints[$field])) {
+ $constraints[$field] = [];
+ }
+ $constraints[$field][$error_id] = [
+ 'message' => $constraint['message'],
+ 'function' => $closure
];
- break;
}
}
- // add constraints based on `usage` attribute
- if(isset($schema[$field]['usage']) && !empty($value)) {
- $constraint = DataValidator::getConstraintFromUsage($schema[$field]['usage']);
- $constraints[$field]['type_misuse'] = [
- 'message' => "Value [$value] does not comply with usage '{$schema[$field]['usage']}'.",
- 'function' => $constraint['rule']
- ];
- }
- // add constraints based on `required` attribute
- /*
- // #memo - has no effect
- if(isset($schema[$field]['required']) && $schema[$field]['required']) {
- $constraints[$field]['missing_mandatory'] = [
- 'message' => 'Missing mandatory value.',
- 'function' => function($a) { return (isset($a) && (!is_string($a) || !empty($a))); }
- ];
- }
- */
- // check constraints
- if(isset($constraints[$field])) {
- foreach($constraints[$field] as $error_id => $constraint) {
- if(isset($constraint['function']) ) {
- $validation_func = $constraint['function'];
- // #todo - use a single arg (validation should be independent from context, otherwise use cancreate/canupdate)
- if(is_callable($validation_func) && !call_user_func($validation_func, $value, $values)) {
- if(!isset($constraint['message'])) {
- $constraint['message'] = 'Invalid field.';
- }
- trigger_error("ORM::given value for field `{$field}` violates constraint : {$constraint['message']}", QN_REPORT_DEBUG);
- $error_code = QN_ERROR_INVALID_PARAM;
- if(!isset($res[$field])) {
- $res[$field] = [];
- }
- $res[$field][$error_id] = $constraint['message'];
- }
+ }
+ // check constraints
+ foreach($values as $field => $value) {
+ if(!isset($constraints[$field]) || empty($constraints[$field])) {
+ // ignore fields with no constraints
+ continue;
+ }
+ if($value === null) {
+ // all fields can be reset to null
+ continue;
+ }
+ foreach($constraints[$field] as $error_id => $constraint) {
+ if(!isset($constraint['function']) ) {
+ continue;
+ }
+ $validation_func = $constraint['function'];
+ // #todo - use a single arg (validation should be independent from context, otherwise use cancreate/canupdate)
+ if(is_callable($validation_func) && !call_user_func($validation_func, $value, $values)) {
+ if(!isset($constraint['message'])) {
+ $constraint['message'] = 'Invalid field.';
}
+ trigger_error("ORM::given value for field `{$field}` violates constraint : {$constraint['message']}", QN_REPORT_INFO);
+ $error_code = QN_ERROR_INVALID_PARAM;
+ if(!isset($res[$field])) {
+ $res[$field] = [];
+ }
+ $res[$field][$error_id] = $constraint['message'];
}
}
}
@@ -1396,7 +1363,7 @@ public function validate($class, $ids, $values, $check_unique=false, $check_requ
elseif(isset($extra_values[$id][$field])) {
$value = $extra_values[$id][$field];
}
- else {
+ elseif(!isset($schema[$field])) {
// ignore non-exiting fields
continue;
}
@@ -1540,7 +1507,6 @@ public function create($class, $fields=null, $lang=null, $use_draft=true) {
/** @var \equal\data\adapt\DataAdapterProvider */
$dap = $this->container->get('adapt');
foreach($creation_array as $field => $value) {
- /** @var \equal\data\adapt\DataAdapter */
$adapter = $dap->get('sql');
$f = new Field($schema[$field]);
// adapt value to SQL
@@ -1641,8 +1607,6 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals
},
ARRAY_FILTER_USE_KEY
);
- // stack current state of object_methods map (we'll restore current state at the end of the update cycle)
- $object_methods_state = $this->object_methods;
// 3) make sure objects in the collection can be updated
@@ -1682,6 +1646,7 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals
// remember callbacks that are triggered by the update
$onupdate_fields = [];
$onrevert_fields = [];
+ $instant_fields = [];
// update internal buffer with given values
foreach($ids as $oid) {
@@ -1699,8 +1664,13 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals
$onupdate_fields[] = $field;
}
}
- elseif(isset($schema[$field]['onrevert'])) {
- $onrevert_fields[] = $field;
+ else {
+ if(isset($schema[$field]['onrevert'])) {
+ $onrevert_fields[] = $field;
+ }
+ if(isset($schema[$field]['instant']) && $schema[$field]['instant']) {
+ $instant_fields[$field] = true;
+ }
}
}
// assign cache to object values
@@ -1738,34 +1708,71 @@ public function update($class, $ids=null, $fields=null, $lang=null, $create=fals
}
}
- // unstack global object_methods state
- $this->object_methods = $object_methods_state;
-
-
// 8) handle the resetting of the dependent computed fields
- $dependencies = [];
+ $dependents = [
+ 'primary' => [],
+ 'related' => []
+ ];
foreach($fields as $field => $value) {
// remember fields whose modification triggers resetting computed fields
+ // #todo - deprecate dependencies : use dependents
if(isset($schema[$field]['dependencies'])) {
- $dependencies = array_merge($dependencies, (array) $schema[$field]['dependencies']);
+ foreach((array) $schema[$field]['dependencies'] as $dependent) {
+ $dependents['primary'][$dependent] = true;
+ }
+ }
+
+ if(isset($schema[$field]['dependents'])) {
+ foreach((array) $schema[$field]['dependents'] as $key => $val) {
+ // handle array notation
+ if(!is_numeric($key)) {
+ if(!isset($dependents['related'][$key])) {
+ $dependents['related'][$key] = [];
+ }
+ $dependents['related'][$key] = array_merge($dependents['related'][$key], (array) $val);
+ }
+ else {
+ $dependents['primary'][$val] = true;
+ }
+ }
}
}
- // remember fields that must be re-computed instantly
- $instant_fields = [];
- foreach(array_unique($dependencies) as $dependency) {
- // #todo - add support for dot notation
- if(isset($schema[$dependency]) && $schema[$dependency]['type'] == 'computed') {
- if(isset($schema[$dependency]['instant']) && $schema[$dependency]['instant']) {
- $instant_fields[] = $dependency;
+
+ // read all target fields at once
+ $this->load($class, $ids, array_keys($dependents['related']), $lang);
+
+ foreach($dependents['related'] as $field => $subfields) {
+ $values = [];
+ foreach($subfields as $subfield) {
+ $values[$subfield] = null;
+ }
+ foreach($ids as $oid) {
+ $target_ids = (array) $this->cache[$table_name][$oid][$lang][$field];
+ // allow cascade update (circular dependencies are checked in `core_test_package`)
+ $this->update($schema[$field]['foreign_object'], $target_ids, $values, $lang);
+ }
+ }
+
+ // handle fields that must be re-computed instantly
+ $values = [];
+ foreach(array_keys($dependents['primary']) as $field) {
+ if(isset($schema[$field]) && $schema[$field]['type'] == 'computed') {
+ if(isset($schema[$field]['instant']) && $schema[$field]['instant']) {
+ $instant_fields[$field] = true;
}
- // allow cascade update
- $this->update($class, $ids, [$dependency => null], $lang, $create);
+ $values[$field] = null;
}
}
+
+ if(count($values)) {
+ // allow cascade update (circular dependencies are checked in `core_test_package`)
+ $this->update($class, $ids, $values, $lang);
+ }
+
if(count($instant_fields)) {
- // re-compute 'instant' computed field
- $this->read($class, $ids, array_unique($instant_fields), $lang);
+ // re-compute local 'instant' computed field
+ $this->load($class, $ids, array_keys($instant_fields), $lang);
}
@@ -1836,6 +1843,7 @@ public function read($class, $ids=null, $fields=null, $lang=null) {
}
// get static instance (check that given class exists)
+ /** @var \equal\orm\Model */
$model = $this->getStaticInstance($class);
$schema = $model->getSchema();
// retrieve name of the DB table associated with the class
@@ -1868,11 +1876,11 @@ public function read($class, $ids=null, $fields=null, $lang=null) {
// handle fields with 'dot' notation
if(strpos($field, '.') > 0) {
$dot_fields[] = $field;
- // drop dot fields (they will be handled later on)
+ // drop dot fields (they are handled in dedicated step)
unset($fields[$key]);
}
// invalid field
- else if(!isset($schema[$field])) {
+ elseif(!isset($schema[$field])) {
// drop invalid fields
unset($fields[$key]);
trigger_error("ORM::unknown field '$field' for class : '$class'", QN_REPORT_WARNING);
@@ -1941,42 +1949,55 @@ public function read($class, $ids=null, $fields=null, $lang=null) {
// 5) handle dot fields
- foreach($dot_fields as $field) {
+ // create a map associating fields with all subfields
+ $map_dot_fields = [];
+ foreach($dot_fields as $path) {
// extract sub field and remainder
- $parts = explode('.', $field, 2);
+ $parts = explode('.', $path, 2);
// left side of the first dot
- $path_field = $parts[0];
- // #todo - use getField
- // retrieve final type of targeted field
- $field_type = $this->getFinalType($class, $path_field);
- // ignore non-relational fields
- if(!$field_type || !in_array($field_type, ['many2one', 'one2many', 'many2many'])) {
- continue;
+ $field = $parts[0];
+ if(!isset($map_dot_fields[$field])) {
+ $map_dot_fields[$field] = [];
}
- // read the field values
- $values = $this->read($class, $ids, (array) $path_field, $lang);
+ $map_dot_fields[$field][] = $parts[1];
+ }
- if($values < 0) {
- continue;
- }
+ // read all dot fields at once
+ $this->load($class, $ids, array_keys($map_dot_fields), $lang);
- // recursively read sub objects
- foreach($ids as $oid) {
- // #memo - unreachable values are always set to null
- $res[$oid][$field] = null;
- if(isset($values[$oid][$path_field]) && isset($schema[$path_field]['foreign_object'])) {
- $sub_class = $schema[$path_field]['foreign_object'];
- $sub_ids = $values[$oid][$path_field];
- $sub_values = $this->read($sub_class, $sub_ids, (array) $parts[1], $lang);
- if($sub_values > 0) {
+ // recursively read sub objects
+ foreach($map_dot_fields as $field => $sub_fields) {
+ foreach($sub_fields as $sub_path) {
+ // retrieve final type of targeted field
+ $f = $model->getField($field);
+ $descriptor = $f->getDescriptor();
+ if(!$descriptor || !isset($descriptor['result_type']) || !isset($descriptor['foreign_object'])) {
+ // ignore invalid descriptors
+ continue;
+ }
+ $field_type = $descriptor['result_type'];
+ if(!$field_type || !in_array($field_type, ['many2one', 'one2many', 'many2many'])) {
+ // ignore non-relational fields
+ continue;
+ }
+ // recursively read sub objects for each object
+ // #todo - this could be improved by loading all targeted objects for current collection at once
+ foreach($ids as $oid) {
+ // #memo - for unreachable values, m2o are always set to null, and m2m or o2m to an empty array
+ $sub_ids = $this->cache[$table_name][$oid][$lang][$field];
+ if(isset($sub_ids) || count($sub_ids)) {
+ $sub_values = $this->read($descriptor['foreign_object'], (array) $sub_ids, (array) $sub_path, $lang);
+ if($sub_values <= 0) {
+ continue;
+ }
if($field_type == 'many2one') {
$odata = reset($sub_values);
if(is_array($odata) || is_a($odata, Model::getType())) {
- $res[$oid][$field] = $odata[$parts[1]];
+ $res[$oid][$field.'.'.$sub_path] = $odata[$sub_path];
}
}
else {
- $res[$oid][$field] = $sub_values;
+ $res[$oid][$field.'.'.$sub_path] = $sub_values;
}
}
}
@@ -2174,7 +2195,7 @@ public function clone($class, $id, $values=[], $lang=null, $parent_field='') {
$original = $res_r[$id];
$new_values = [];
- // unset relations + id and parent_field (needs to be updated + could be part of unique contrainst)
+ // unset relations + id and parent_field (needs to be updated + could be part of unique constraint)
foreach($original as $field => $value) {
$def = $schema[$field];
if(!in_array($def['type'], ['one2many', 'many2many']) && !in_array($field, ['id', $parent_field])) {
@@ -2233,6 +2254,50 @@ public function clone($class, $id, $values=[], $lang=null, $parent_field='') {
return $res;
}
+ /**
+ * Increment the field of an object by a given increment (integer value).
+ *
+ * note : We use this nomenclature because it has a recognized semantics and equivalence in many programming languages.
+ *
+ * @param string $class Class name of the object to clone.
+ * @param array $ids Array of ids of the objects to update.
+ * @param string $field Name of the field to increment (must have a numeric type).
+ * @param integer $increment Value by witch increment the field (positive or negative).
+ *
+ * @return int|array Returns an array of updated ids, or an error identifier in case an error occurred.
+ */
+ public function fetchAndAdd($class, $ids, $field, $increment) {
+ $result = [];
+ $db = $this->getDBHandler();
+ try {
+ // get static instance (checks that given class exists)
+ $object = $this->getStaticInstance($class);
+ // retrieve schema
+ $schema = $object->getSchema();
+ // make sure that the targeted field exists and has a numeric type
+ if(!isset($schema[$field])) {
+ throw new Exception('unknown_field', EQ_ERROR_INVALID_PARAM);
+ }
+ if(!in_array($schema[$field]['type'], ['integer', 'float'])) {
+ throw new Exception('non_numeric_field', EQ_ERROR_INVALID_PARAM);
+ }
+ $table_name = $this->getObjectTableName($class);
+ // increment the field as an atomic operation
+ $res = $db->incRecords($table_name, $ids, $field, $increment);
+ // #todo - returned values must be similar to a
+ while ($row = $db->fetchArray($res)) {
+ // maintain ids order provided by the SQL sort
+ $result[$row['id']] = $row[$field];
+ }
+ }
+ catch(Exception $e) {
+ trigger_error("ORM::".$e->getMessage(), QN_REPORT_WARNING);
+ $this->last_error = $e->getMessage();
+ $result = $e->getCode();
+ }
+ return $result;
+ }
+
/**
* Returns applicable transitions based on a list of updated fields, according to the dependencies defined in the related workflow descriptors.
* If no workflow is defined for the given class, an empty array is returned.
@@ -2416,7 +2481,7 @@ public function search($class, $domain=null, $sort=['id' => 'asc'], $start='0',
$table_name = $this->getObjectTableName($class);
- // we use a nested closure to define a function that stores original table names and returns corresponding aliases
+ // use nested closure to store original table names and return corresponding aliases
$add_table = function ($table_name) use (&$tables) {
if(in_array($table_name, $tables)) {
return array_search($table_name, $tables);
@@ -2644,11 +2709,15 @@ public function search($class, $domain=null, $sort=['id' => 'asc'], $start='0',
if($schema[$sort_field]['type'] == 'many2one' || (isset($schema[$sort_field]['result_type']) && $schema[$sort_field]['result_type'] == 'many2one') ) {
$related_table = $this->getObjectTableName($schema[$sort_field]['foreign_object']);
$related_table_alias = $add_table($related_table);
- $select_fields[] = $related_table_alias.'.name';
+ $related_schema = $this->getObjectSchema($schema[$sort_field]['foreign_object']);
// #todo - shouldn't this condition be added to all clauses?
$conditions[0][] = array($table_alias.'.'.$sort_field, '=', '`'.$related_table_alias.'.id'.'`');
$order_table_alias = $related_table_alias;
$sort_field = 'name';
+ while($related_schema[$sort_field]['type'] == 'alias') {
+ $sort_field = $related_schema[$sort_field]['alias'];
+ }
+ $select_fields[] = $related_table_alias.'.'.$sort_field;
}
else {
$select_fields[] = $table_alias.'.'.$sort_field;
diff --git a/lib/equal/orm/UsageFactory.class.php b/lib/equal/orm/UsageFactory.class.php
index 1cbb06a1a..960faf67d 100644
--- a/lib/equal/orm/UsageFactory.class.php
+++ b/lib/equal/orm/UsageFactory.class.php
@@ -77,10 +77,13 @@ public static function create(string $usage): Usage {
// datetime usages
case 'date':
$usageInstance = new UsageDate($usage);
+ break;
case 'time':
$usageInstance = new UsageTime($usage);
+ break;
case 'email':
$usageInstance = new UsageEmail($usage);
+ break;
case 'hash':
break;
// binary usages
diff --git a/lib/equal/orm/usages/Usage.class.php b/lib/equal/orm/usages/Usage.class.php
index 5ad2fef56..0117caa26 100644
--- a/lib/equal/orm/usages/Usage.class.php
+++ b/lib/equal/orm/usages/Usage.class.php
@@ -51,6 +51,11 @@ class Usage {
*/
protected $size = 0;
+ /**
+ * Return the constraints descriptors, according to the Usage instance.
+ * Since `function` properties returned by this method expect a non-static context,
+ * using the ORM, those callbacks are bound to a Usage instance using `bindTo()`.
+ */
public function getConstraints(): array {
return [];
}
diff --git a/lib/equal/orm/usages/UsageAmount.class.php b/lib/equal/orm/usages/UsageAmount.class.php
index 37cdf9d73..984599170 100644
--- a/lib/equal/orm/usages/UsageAmount.class.php
+++ b/lib/equal/orm/usages/UsageAmount.class.php
@@ -45,10 +45,7 @@ public function getConstraints(): array {
case 'money':
case 'percent':
case 'rate':
- if(preg_match('/^[+-]?[0-9]{0,9}(\.[0-9]{0,'.$scale.'})?$/', (string) $value)) {
- return false;
- }
- break;
+ return preg_match('/^[+-]?[0-9]{0,9}(\.[0-9]{0,'.$scale.'})?$/', (string) $value);
}
return true;
}
diff --git a/lib/equal/orm/usages/UsageCountry.class.php b/lib/equal/orm/usages/UsageCountry.class.php
index 249441d2a..b4f3322c9 100644
--- a/lib/equal/orm/usages/UsageCountry.class.php
+++ b/lib/equal/orm/usages/UsageCountry.class.php
@@ -20,7 +20,7 @@ public function getConstraints(): array {
]
];
}
- // subtype is expected to be iso-3166
+ // subtype is expected to be ISO-3166
switch($this->getLength()) {
case '3':
return [
@@ -42,7 +42,6 @@ public function getConstraints(): array {
}
]
];
-
}
}
diff --git a/lib/equal/orm/usages/UsageCurrency.class.php b/lib/equal/orm/usages/UsageCurrency.class.php
index df0bb0ce2..9fa84feaa 100644
--- a/lib/equal/orm/usages/UsageCurrency.class.php
+++ b/lib/equal/orm/usages/UsageCurrency.class.php
@@ -13,7 +13,7 @@ public function getConstraints(): array {
if($this->getSubtype() == 'iso-4217.numeric') {
return [
'invalid_currency' => [
- 'message' => 'Value is not a 3-digits country code (iso-3166-1).',
+ 'message' => 'Value is not a 3-digits country code (as of iso-3166-1).',
'function' => function($value) {
return (in_array($value, ['004','008','010','012','016','020','024','028','031','032','036','040','044','048','050','051','052','056','060','064','068','070','072','074','076','084','086','090','092','096','100','104','108','112','116','120','124','132','136','140','144','148','152','156','158','162','166','170','174','175','178','180','184','188','191','192','196','203','204','208','212','214','218','222','226','231','232','233','234','238','239','242','246','248','250','254','258','260','262','266','268','270','275','','276','288','292','296','300','304','308','312','316','320','324','328','332','334','336','340','344','348','352','356','360','364','368','372','376','380','384','388','392','398','400','404','408','410','414','417','418','422','426','428','430','434','438','440','442','446','450','454','458','462','466','470','474','478','480','484','492','496','498','499','500','504','508','512','516','520','524','528','531','533','534','535','540','548','554','558','562','566','570','574','578','580','581','583','584','585','586','591','598','600','604','608','612','616','620','624','626','630','634','638','642','643','646','652','654','659','660','662','663','666','670','674','678','682','686','688','690','694','702','703','704','705','706','710','716','724','728','729','732','740','744','748','752','756','760','762','764','768','772','776','780','784','788','792','795','796','798','800','804','807','818','826','831','832','833','834','840','850','854','858','860','862','876','882','887','894']));
}
@@ -27,7 +27,7 @@ public function getConstraints(): array {
default:
return [
'invalid_currency' => [
- 'message' => 'Value is not a 2-letters language code (iso-4217 alpha-3).',
+ 'message' => 'Value is not a 2-letters language code (as of iso-4217 alpha-3).',
'function' => function($value) {
return (in_array($value, ['ADF','ADP','AED','AFA','AFN','ALL','AMD','ANG','AOA','AOK','AON','AOR','ARP','ARS','ATS','AUD','AWG','AZM','AZN','BAM','BBD','BDT','BEF','BGL','BGN','BHD','BIF','BMD','BND','BOB','BOP','BOV','BRL','BRR','BSD','BTN','BWP','BYB','BYR','BYN','BZD','CAD','CDF','CHE','CHF','CHW','CLF','CLP','CNY','COP','COU','CRC','CSD','CSK','CUC','CUP','CVE','CYP','CZK','DEM','DJF','DKK','DOP','DZD','ECS','ECV','EEK','EGP','ERN','ESP','ETB','EUR','FIM','FJD','FKP','FRF','GBP','GEL','GHS','GIP','GMD','GNF','GRD','GTQ','GWP','GYD','HKD','HNL','HRK','HTG','HUF','IDR','IEP','ILS','INR','IQD','IRR','ISK','ITL','JMD','JOD','JPY','KES','KGS','KHR','KMF','KPW','KRW','KZT','KWD','KYD','LAK','LBP','LKR','LRD','LSL','LTL','LUF','LVL','LVR','LYD','MAD','MDL','MGA','MGF','MKD','MMK','MNT','MOP','MRO','MRU','MTL','MUR','MVR','MWK','MXN','MXV','MYR','MZE','MZM','MZN','NAD','NGN','NHF','NIC','NIO','NLG','NOK','NPR','NZD','OMR','PAB','PEN','PES','PGK','PHP','PKR','PLN','PLZ','PTE','PYG','QAR','ROL','RON','RSD','RUB','RWF','SAR','SBD','SCR','SDD','SDG','SDP','SEK','SGD','SHP','SIT','SKK','SLL','SML','SOS','SRD','SSP','STD','SUB','SUR','SVC','SYP','SZL','THB','TJS','TMM','TMT','TND','TOP','TPE','TRL','TRY','TTD','TWD','TZS','UAH','UGX','USD','USN','USS','UYU','UYW','UZS','VAL','VEB','VEF','VES','VND','VUV','WST','XAF','XAG','XAU','XBA','XBB','XBC','XBD','XCD','XDR','XEU','XFO','XFU','XOF','XPD','XPF','XPT','XSU','XUA','YER','YUD','YUM','ZAR','ZMK','ZWD','ZWL','ZWR']));
}
diff --git a/lib/equal/orm/usages/UsageDate.class.php b/lib/equal/orm/usages/UsageDate.class.php
index 6c901b553..7d2bfda90 100644
--- a/lib/equal/orm/usages/UsageDate.class.php
+++ b/lib/equal/orm/usages/UsageDate.class.php
@@ -30,7 +30,7 @@ public function getConstraints(): array {
case 'day':
return [
'invalid_amount' => [
- 'message' => 'Malformed amount or size overflow.',
+ 'message' => 'Malformed day value.',
'function' => function($value) {
// 2 digits, from 1 to 31
return ($value >= 0 && $value <= 31);
@@ -40,7 +40,7 @@ public function getConstraints(): array {
case 'month':
return [
'invalid_amount' => [
- 'message' => 'Malformed amount or size overflow.',
+ 'message' => 'Malformed month value.',
'function' => function($value) {
// 2 digits, from 1 to 12
return ($value >= 0 && $value <= 12);
@@ -50,7 +50,7 @@ public function getConstraints(): array {
case 'year':
return [
'invalid_amount' => [
- 'message' => 'Malformed amount or size overflow.',
+ 'message' => 'Malformed year value.',
'function' => function($value) {
// 4 digits, from 0 to 9999
return ($value >= 0 && $value <= 9999);
diff --git a/lib/equal/orm/usages/UsageNumber.class.php b/lib/equal/orm/usages/UsageNumber.class.php
index 1a9e3d5e4..f51d024d8 100644
--- a/lib/equal/orm/usages/UsageNumber.class.php
+++ b/lib/equal/orm/usages/UsageNumber.class.php
@@ -66,13 +66,10 @@ public function getConstraints(): array {
switch($this->getSubtype()) {
case 'boolean':
return [
- 'not_integer' => [
+ 'not_boolean' => [
'message' => 'Value is not a boolean.',
'function' => function($value) {
- if(!preg_match('/^[0-1]$/', (string) $value)) {
- return false;
- }
- return true;
+ return preg_match('/^[0-1]$/', (string) intval($value));
}
]
];
@@ -84,10 +81,7 @@ public function getConstraints(): array {
$len = intval($this->getLength());
// if length is empty, default to 18 (max)
$len = ($len)?$len:18;
- if(!preg_match('/^[+-]?[0-9]{0,'.$len.'}$/', (string) $value)) {
- return false;
- }
- return true;
+ return preg_match('/^[+-]?[0-9]{0,'.$len.'}$/', (string) $value);
}
]
];
@@ -106,10 +100,7 @@ public function getConstraints(): array {
// expected len format is `precision.scale`
$scale = $this->getScale();
$integers = $this->getPrecision();
- if(preg_match('/^[+-]?[0-9]{0,'.$integers.'}(\.[0-9]{1,'.$scale.'})?$/', (string) $value)) {
- return false;
- }
- return true;
+ return preg_match('/^[+-]?[0-9]{0,'.$integers.'}(\.[0-9]{1,'.$scale.'})?$/', (string) $value);
}
]
];
@@ -121,10 +112,7 @@ public function getConstraints(): array {
$len = intval($this->getLength());
// if length is empty, default to 255 (max)
$len = ($len)?$len:255;
- if(preg_match('/^[0-9A-F]{0,'.$len.'}$/', (string) $value)) {
- return false;
- }
- return true;
+ return preg_match('/^[0-9A-F]{0,'.$len.'}$/', (string) $value);
}
]
];
diff --git a/lib/equal/orm/usages/UsageText.class.php b/lib/equal/orm/usages/UsageText.class.php
index 56a82cec2..ff3988da2 100644
--- a/lib/equal/orm/usages/UsageText.class.php
+++ b/lib/equal/orm/usages/UsageText.class.php
@@ -18,12 +18,6 @@ public function __construct(string $usage_str) {
public function getConstraints(): array {
return [
- 'not_string_type' => [
- 'message' => 'Value is not a string.',
- 'function' => function($value) {
- return (gettype($value) == 'string');
- }
- ],
'size_exceeded' => [
'message' => 'String exceeds usage length constraint.',
'function' => function($value) {
@@ -42,16 +36,16 @@ public function getConstraints(): array {
case 'plain':
break;
case 'html':
- $doc = new DOMDocument();
+ $doc = new \DOMDocument();
libxml_use_internal_errors(true);
$doc->loadHTML($value);
return (empty(libxml_get_errors()));
break;
case 'xml':
// #todo - check XML validity
- $xml = new XMLReader();
+ $xml = new \XMLReader();
$xml->xml($value);
- $xml->setParserProperty(XMLReader::VALIDATE, true);
+ $xml->setParserProperty(\XMLReader::VALIDATE, true);
return $xml->isValid();
case 'markdown':
// #todo - check markdown validity
diff --git a/lib/equal/services/Container.class.php b/lib/equal/services/Container.class.php
index 24041178f..dbae7be12 100644
--- a/lib/equal/services/Container.class.php
+++ b/lib/equal/services/Container.class.php
@@ -64,17 +64,26 @@ private function inject($dependency) {
if(count($parameters)) {
foreach($parameters as $parameter) {
- // #deprecated
- // $constructor_dependency = $parameter->getClass()->getName();
- $constructor_dependency = $parameter->getType()->getName();
- // #todo - missing cyclic dependency check
- $res = $this->inject($constructor_dependency);
- if(count($res[1])) {
- $unresolved_dependencies = array_merge($unresolved_dependencies, $res[1]);
+ // #memo - ReflectionType::__toString has been deprecated PHP 7.4 and undeprecated in 8.0
+ /** @var ReflectionType */
+ $type_name = @ (string) $parameter->getType();
+ // ignore scalar types
+ if(empty($type_name) || in_array($type_name, ['array', 'bool', 'callable', 'float', 'int', 'null', 'object', 'string', 'false', 'iterable', 'mixed', 'never', 'true', 'void'])) {
continue;
}
- if($res[0] instanceof $constructor_dependency) {
- $dependencies_instances[] = $res[0];
+ if($type_name == 'equal\services\Container') {
+ $dependencies_instances[] = $this;
+ }
+ else {
+ // #todo - add support for cyclic dependency detection
+ $res = $this->inject($type_name);
+ if(count($res[1])) {
+ $unresolved_dependencies = array_merge($unresolved_dependencies, $res[1]);
+ continue;
+ }
+ if($res[0] instanceof $type_name) {
+ $dependencies_instances[] = $res[0];
+ }
}
}
}
diff --git a/packages/core/actions/config/create-workflow.php b/packages/core/actions/config/create-workflow.php
index ed98cd6d1..ea3ba1257 100644
--- a/packages/core/actions/config/create-workflow.php
+++ b/packages/core/actions/config/create-workflow.php
@@ -4,7 +4,6 @@
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU LGPL 3 license
*/
-use PhpParser\ParserFactory;
list($params, $providers) = eQual::announce([
'description' => "Add an empty workflow to the given class by creating a `getWorkflow()` method (if not defined yet).",
@@ -33,36 +32,27 @@
*/
list($context, $orm) = [ $providers['context'], $providers['orm'] ];
-
-// retrieve target entity
+// force class autoload
$entity = $orm->getModel($params['entity']);
if(!$entity) {
throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM);
}
-$reflectionClass = new ReflectionClass($entity::getType());
-if($reflectionClass->getMethod('getWorkflow')->class == $entity::getType()) {
+$class = new ReflectionClass($entity::getType());
+if($class->getMethod('getWorkflow')->class == $entity::getType()) {
throw new Exception("duplicate_method", QN_ERROR_INVALID_PARAM);
}
-$parts = explode('\\', $params['entity']);
-// Get the package name from the first part of the string
-$package = array_shift($parts);// Get the package name from the first part of the string
-// Get the file name from the last part of the string
-$class_name = array_pop($parts);
-// Get the class path from the remaining part
-$class_path = implode('/', $parts);
-
-// Create all the object to use for using PhpParser
-$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
-$printer = new PhpParser\PrettyPrinter\Standard;
-
-// Get the full path of the file
-$file = QN_BASEDIR."/packages/{$package}/classes/{$class_path}/{$class_name}.class.php";
-
-// get php code from original file ...
+$file = $class->getFileName();
$code = file_get_contents($file);
+// generate replacement code for the getWorkflow method
+$workflow_code = ''.
+ " public static function getWorkflow() {\n".
+ " return [];\n".
+ " }\n".
+ "\n";
+
// find the closing curly-bracket of the class
$pos = strrpos($code, '}');
@@ -70,33 +60,13 @@
throw new Exception('malformed_file', QN_ERROR_UNKNOWN);
}
-$code = substr_replace($code, 'public static function getWorkflow() {return [];} }', $pos, 1);
-
-// ... and parse it to create an AST
-$stmt = $parser->parse($code);
+$result = substr_replace($code, $workflow_code, $pos, 0);
-// Pretty print the modified AST ...
-$result = $printer->prettyPrintFile($stmt);
-
-// ... and write back the code to the file
-if(file_put_contents($file, $result) === false) {
+// write back the code to the source file
+if(file_put_contents($file, rtrim($result)."\n") === false) {
throw new Exception('io_error', QN_ERROR_UNKNOWN);
}
-try {
- // apply coding standards (ecs.php is expected in QN_BASEDIR)
- $command = 'php ./vendor/bin/ecs check "' . str_replace('\\', '/', $file) . '" --fix';
- if(exec($command) === false) {
- throw new Exception('command_failed', QN_ERROR_UNKNOWN);
- }
-}
-catch(Exception $e) {
- trigger_error("PHP::unable to beautify rendered file ($file): ".$e->getMessage(), QN_REPORT_INFO);
-}
-
-$result = file_get_contents($file);
-
$context->httpResponse()
->status(204)
- ->body($result)
->send();
diff --git a/packages/core/actions/config/update-uml.php b/packages/core/actions/config/update-uml.php
index 78d2a4aca..82a06bda1 100644
--- a/packages/core/actions/config/update-uml.php
+++ b/packages/core/actions/config/update-uml.php
@@ -9,7 +9,7 @@
'required' => true
],
'path' => [
- 'decription' => 'relative path to the file from packages/{pkg}/',
+ 'description' => 'relative path to the file from packages/{pkg}/',
'type' => 'string',
'required' => true
],
@@ -23,7 +23,7 @@
'type' => 'string',
'required' => true,
'selection' => [
- 'or'
+ 'erd'
]
]
],
@@ -68,13 +68,13 @@
$str_payload =$params['payload'];
-if(!endsWith($filename,".{$params["type"]}.equml")) {
- $filename = $filename.".{$params["type"]}.equml";
+if(!endsWith($filename,".{$params["type"]}.json")) {
+ $filename = $filename.".{$params["type"]}.json";
}
if(!is_dir(QN_BASEDIR."/packages/{$package}/uml/{$path}")) {
$response_code = 201;
- if(!mkdir(QN_BASEDIR."/packages/{$package}/uml/{$path}",0775,true)) {
+ if(!mkdir(QN_BASEDIR."/packages/{$package}/uml/{$path}", 0775, true)) {
throw new Exception('io_error'.QN_BASEDIR."/packages/{$package}/uml/{$path}", QN_ERROR_INVALID_CONFIG);
}
}
@@ -82,11 +82,13 @@
if($response_code === 200 && !file_exists(QN_BASEDIR."/packages/{$package}/uml/{$path}/{$filename}")) {
$response_code = 201;
}
-// Create file
+
+// create file
$f = fopen(QN_BASEDIR."/packages/{$package}/uml/{$path}/{$filename}","w");
if(!$f) {
throw new Exception('io_error', QN_ERROR_INVALID_CONFIG);
}
+
fputs($f,$str_payload);
fclose($f);
@@ -95,7 +97,7 @@
$context->httpResponse()
->body($result)
->status($response_code)
- ->send();
+ ->send();
function endsWith( $haystack, $needle ) {
$length = strlen( $needle );
diff --git a/packages/core/actions/config/update-workflow.php b/packages/core/actions/config/update-workflow.php
index 82dec5fa3..65f8f6aee 100644
--- a/packages/core/actions/config/update-workflow.php
+++ b/packages/core/actions/config/update-workflow.php
@@ -9,11 +9,6 @@
list($params, $providers) = eQual::announce([
'description' => "Translate a workflow definition of a given entity to a PHP method and store it in related file.",
'help' => "This controller rely on the PHP binary. In order to make them work, sure the PHP binary is present in the PATH.",
- 'response' => [
- 'content-type' => 'text/plain',
- 'charset' => 'UTF-8',
- 'accept-origin' => '*'
- ],
'params' => [
'entity' => [
'description' => 'Name of the entity (class).',
@@ -26,6 +21,11 @@
'required' => true
]
],
+ 'response' => [
+ 'content-type' => 'text/plain',
+ 'charset' => 'UTF-8',
+ 'accept-origin' => '*'
+ ],
'providers' => ['context', 'orm']
]);
@@ -35,88 +35,96 @@
*/
list($context, $orm) = [$providers['context'], $providers['orm']];
-// Create all the object to use for using PhpParser
-$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
-$nodeFinder = new NodeFinder;
-$traverser = new NodeTraverser;
-$prettyPrinter = new PhpParser\PrettyPrinter\Standard;
-// Get the parts of the entity string, separated by backslashes
-$parts = explode('\\', $params['entity']);
-// Get the package name from the first part of the string
-$package = array_shift($parts);// Get the package name from the first part of the string
-// Get the file name from the last part of the string
-$filename = array_pop($parts);
-// Get the class path from the remaining part
-$class_path = implode('/', $parts);
-
-$getColumns = null;
-$code_php = null;
+// force class autoload
+$entity = $orm->getModel($entity::getType());
+if(!$entity) {
+ throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM);
+}
-// Decode the JSON string to a PHP object
-$code_php = $params['payload'];
-// Get a string representation from the code_php variable, with backslashes escaped
-$code_string = str_replace("\\\\", "\\", var_export($code_php, true));
-// Create a virtual file holing the default structure with the parsed code and generate a minimal AST
-$ast_temp = $parser->parse("findFirst(
- $ast_temp,
- function(Node $node) {
- return isset($node->name->name)
- && $node->name->name === "getWorkflow";
- }
-);
+$class = new ReflectionClass($entity::getType());
-// Add a visitor hijack the getWorkflow method and update its content with new schema
-$traverser->addVisitor(
- new class($nodeGetWorkflow, "getWorkflow") extends NodeVisitorAbstract {
- private $node;
- private $target;
- public function __construct($node, $target) {
- $this->node = $node;
- $this->target = $target;
- }
- public function leaveNode(Node $node) {
- if (
- isset($node->name->name)
- && $node->name->name === $this->target
- ) {
- return $this->node;
- }
+if(!$class->hasMethod('getWorkflow')) {
+ throw new Exception('missing_workflow', QN_ERROR_UNKNOWN);
+}
- return null;
- }
- }
- );
+$getWorkflow = new ReflectionMethod($params['entity'], 'getWorkflow');
-// Get the full path of the file
-$file = QN_BASEDIR."/packages/{$package}/classes/{$class_path}/{$filename}.class.php";
-// Get the code from the original file ...
+// retrieve content of the source file of the targeted entity
+$file = $class->getFileName();
$code = file_get_contents($file);
-// ... and parse it to create an AST
-$stmtOriginal = $parser->parse($code);
-// Update the AST by using visitors attached to the traverser
-$stmtModified = $traverser->traverse($stmtOriginal);
-// Pretty print the modified AST ...
-$result = $prettyPrinter->prettyPrintFile($stmtModified);
-// ... and write back the code to the file
-if(file_put_contents($file, $result) === false) {
- throw new Exception('io_error', QN_ERROR_UNKNOWN);
-}
+$lines = explode("\n", $code);
+
+// retrieve start and end lines of getWorkflow declaration
+$start_index = $getWorkflow->getStartLine() - 1;
+$end_index = $getWorkflow->getEndLine() - 1;
+
+// generate replacement code for the getWorkflow method
+$workflow_code = ''.
+ " public static function getWorkflow() {\n".
+ " return ".array_export($params['payload'], 4, 2, true).";\n".
+ " }";
-try {
- // apply coding standards (ecs.php is expected in QN_BASEDIR)
- $command = 'php ./vendor/bin/ecs check "' . str_replace('\\', '/', $file) . '" --fix';
- if(exec($command) === false) {
- throw new Exception('command_failed', QN_ERROR_UNKNOWN);
+$result = '';
+
+foreach($lines as $index => $line) {
+ if($index < $start_index) {
+ $result .= $line."\n";
+ }
+ else {
+ if($index == $start_index) {
+ $result .= $workflow_code."\n";
+ }
+ else {
+ if($index > $end_index) {
+ $result .= $line."\n";
+ }
+ }
}
- $result = file_get_contents($file);
}
-catch(Exception $e) {
- trigger_error("PHP::unable to beautify rendered file ($file): ".$e->getMessage(), QN_REPORT_INFO);
+
+// write back the code to the source file
+if(file_put_contents($file, rtrim($result)."\n") === false) {
+ throw new Exception('io_error', QN_ERROR_UNKNOWN);
}
$context->httpResponse()
->status(204)
- ->body($result)
->send();
+
+/**
+ * Use var_export() native function and apply a few regex replacements to improve the syntax.
+ * Note : this strategy is preferred to the use of PhpParser since it allows to modify only the portion of code targeted by the getWorkflow method.
+ *
+ * @param array $array Array to be converted to PHP code.
+ * @param int $indent_spaces Number of spaces to use for code indentation.
+ * @param int $pad_indents Number of indents each line must be prefixed with.
+ * @param bool $ignore_first_indent Flag for disabling indent for first line.
+ */
+function array_export($array, $indent_spaces = 4, $pad_indents = 0, $ignore_first_indent=false) {
+ // convert to PHP code
+ $export = var_export($array, true);
+ // minimalist code 'beautify'
+ $patterns = [
+ // convert to square bracket notation
+ "/array \(/" => '[',
+ "/^([ ]*)\)(,?)$/m" => '$1]$2',
+ "/=>[ ]?\n[ ]+\[/" => '=> [',
+ "/([ ]*)(\'[^\']+\') => ([\[\'])/" => '$1$2 => $3',
+ // remove explicit numeric index
+ "/[0-9]+ => /" => ''
+ ];
+ $result = preg_replace(array_keys($patterns), array_values($patterns), $export);
+ // indent lines
+ $lines = explode("\n", $result);
+ foreach($lines as $index => $line) {
+ if(!$ignore_first_indent || $index > 0) {
+ $code = ltrim($line);
+ // produced PHP code is 2 spaces indented
+ $indents = (strlen($line) - strlen($code)) / 2;
+ $lines[$index] = str_pad('', $pad_indents*$indent_spaces, ' ').
+ str_pad('', $indents*$indent_spaces, ' ').
+ $code;
+ }
+ }
+ return implode("\n", $lines);
+}
diff --git a/packages/core/actions/init/db.php b/packages/core/actions/init/db.php
index 5c7dd9a5a..ac249a8ce 100644
--- a/packages/core/actions/init/db.php
+++ b/packages/core/actions/init/db.php
@@ -4,7 +4,7 @@
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU LGPL 3 license
*/
-use equal\db\DBConnection;
+use equal\db\DBConnector;
list($params, $providers) = eQual::announce([
@@ -19,14 +19,14 @@
eQual::run('do', 'test_db-connectivity');
// create Master database
-$db = DBConnection::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect(false);
+$db = DBConnector::getInstance()->connect(false);
$db->createDatabase(constant('DB_NAME'));
// create replica members, if any
if(defined('DB_REPLICATION') && constant('DB_REPLICATION') != 'NO') {
$i = 1;
while(defined('DB_'.$i.'_HOST') && defined('DB_'.$i.'_PORT') && defined('DB_'.$i.'_USER') && defined('DB_'.$i.'_PASSWORD') && defined('DB_'.$i.'_NAME')) {
- $db = DBConnection::getInstance(constant('DB_'.$i.'_HOST'), constant('DB_'.$i.'_PORT'), constant('DB_'.$i.'_NAME'), constant('DB_'.$i.'_USER'), constant('DB_'.$i.'_PASSWORD'), constant('DB_DBMS'))->connect(false);
+ $db = DBConnector::getInstance(constant('DB_'.$i.'_HOST'), constant('DB_'.$i.'_PORT'), constant('DB_'.$i.'_NAME'), constant('DB_'.$i.'_USER'), constant('DB_'.$i.'_PASSWORD'), constant('DB_DBMS'))->connect(false);
$db->createDatabase(constant('DB_'.$i.'_NAME'));
++$i;
}
diff --git a/packages/core/actions/init/import.php b/packages/core/actions/init/import.php
new file mode 100644
index 000000000..3339f1510
--- /dev/null
+++ b/packages/core/actions/init/import.php
@@ -0,0 +1,333 @@
+
+ Some Rights Reserved, eQual framework, 2010-2024
+ Original author(s): Lucas LAURENT
+ License: GNU LGPL 3 license
+*/
+use equal\db\DBConnection;
+use equal\db\DBManipulator;
+use equal\db\DBConnector;
+
+list($params, $providers) = eQual::announce([
+ 'description' => 'Import data from a database to eQual database for a given package.',
+ 'help' => 'Needs a configuration file `import-config.json` in the `init` folder of the given package.',
+ 'constants' => ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_DBMS'],
+ 'params' => [
+ 'db_dbms' => [
+ 'type' => 'string',
+ 'default' => 'MYSQL'
+ ],
+
+ 'db_host' => [
+ 'type' => 'string',
+ 'required' => true
+ ],
+
+ 'db_port' => [
+ 'type' => 'integer',
+ 'required' => true
+ ],
+
+ 'db_user' => [
+ 'type' => 'string',
+ 'required' => true
+ ],
+
+ 'db_password' => [
+ 'type' => 'string',
+ 'required' => true
+ ],
+
+ 'db_name' => [
+ 'type' => 'string',
+ 'required' => true
+ ],
+
+ 'db_charset' => [
+ 'type' => 'string',
+ 'default' => 'utf8mb4'
+ ],
+
+ 'db_collation' => [
+ 'type' => 'string',
+ 'default' => 'utf8mb4_unicode_ci'
+ ],
+
+ 'package' => [
+ 'type' => 'string',
+ 'required' => true
+ ],
+
+ 'entity' => [
+ 'type' => 'string'
+ ]
+ ],
+ 'response' => [
+ 'content-type' => 'application/json',
+ 'charset' => 'utf-8',
+ 'accept-origin' => '*'
+ ],
+ 'providers' => ['context', 'adapt']
+]);
+
+/**
+ * @var \equal\php\Context $context
+ * @var \equal\data\DataAdapterProvider $dap
+ */
+list($context, $dap) = [$providers['context'], $providers['adapt']];
+
+$getImportConfig = function($config_file_path): array {
+ $config_file_content = file_get_contents($config_file_path);
+ if(!$config_file_content) {
+ throw new Exception('Missing import config file ' . $config_file_path, QN_ERROR_INVALID_CONFIG);
+ }
+
+ $import_config = json_decode($config_file_content, true);
+ if(!is_array($import_config)) {
+ throw new Exception('Invalid import configuration file', QN_ERROR_INVALID_CONFIG);
+ }
+
+ return $import_config;
+};
+
+$createOldDbConnection = function(string $dbms, string $host, int $port, string $name, string $user, string $password, string $charset, string $collation) {
+ $db_connection = DBConnection::create($dbms,$host, $port, $name, $user, $password, $charset, $collation);
+
+ $db_connection->connect();
+ if(!$db_connection->connected()) {
+ throw new Exception(
+ 'Unable to establish connection to DBMS host (wrong credentials) for old connection',
+ QN_ERROR_INVALID_CONFIG
+ );
+ }
+
+ return $db_connection;
+};
+
+$createNewDbConnection = function() {
+ $db_connection = DBConnector::getInstance();
+
+ $db_connection->connect();
+ if(!$db_connection->connected()) {
+ throw new Exception('Unable to establish connection to DBMS host (wrong credentials)', QN_ERROR_INVALID_CONFIG);
+ }
+
+ return $db_connection;
+};
+
+$castOldDbRow = function(array $old_item, array $old_table_fields_config): array {
+ $old_item_casted = [];
+ foreach($old_item as $key => $column_value) {
+ switch($old_table_fields_config[$key]) {
+ case 'integer':
+ $old_item_casted[$key] = (int) $column_value;
+ break;
+ case 'float':
+ $old_item_casted[$key] = (float) $column_value;
+ break;
+ case 'boolean':
+ $old_item_casted[$key] = (bool) $column_value;
+ break;
+ case 'date':
+ $old_item_casted[$key] = strtotime($column_value);
+ break;
+ default:
+ $old_item_casted[$key] = $column_value;
+ break;
+ }
+ }
+
+ return $old_item_casted;
+};
+
+$createNewItemFromOld = function(array $config, DBManipulator $old_db_connection, array $old_item): array {
+ $item = [
+ 'creator' => QN_ROOT_USER_ID,
+ 'created' => time(),
+ 'modifier' => QN_ROOT_USER_ID,
+ 'modified' => time(),
+ 'deleted' => false,
+ 'state' => 'instance'
+ ];
+
+ foreach($config['data_map'] as $new_key => $import_conf) {
+ if(is_string($import_conf)) {
+ $import_conf = [
+ 'type' => 'field',
+ 'field' => $import_conf
+ ];
+ }
+
+ $imp_confs = isset($import_conf['type']) ? [$import_conf] : $import_conf;
+ $field = $imp_confs[0]['field'] ?? null;
+
+ if(!$field) {
+ continue;
+ }
+
+ $previous_value = $old_item[$field] ?? null;
+ foreach($imp_confs as $imp_conf) {
+ switch($imp_conf['type']) {
+ case 'value':
+ $item[$new_key] = $import_conf['value'];
+ break;
+ case 'field':
+ $item[$new_key] = $previous_value;
+ break;
+ case 'computed':
+ $item[$new_key] = $imp_conf['value'];
+ foreach($imp_conf['fields'] as $f) {
+ $item[$new_key] = str_replace('%'.$f.'%', $old_item[$f], $item[$new_key]);
+ }
+ break;
+ case 'cast':
+ switch($imp_conf['cast']) {
+ case 'integer':
+ $item[$new_key] = (int) $previous_value;
+ break;
+ case 'boolean':
+ $item[$new_key] = (bool) $previous_value;
+ break;
+ case 'string':
+ $item[$new_key] = (string) $previous_value;
+ break;
+ }
+ break;
+ case 'round':
+ $item[$new_key] = round($previous_value);
+ break;
+ case 'multiply':
+ $item[$new_key] = $previous_value * $import_conf['by'];
+ break;
+ case 'divide':
+ $item[$new_key] = $previous_value / $import_conf['by'];
+ break;
+ case 'field-contains':
+ $item[$new_key] = strpos($previous_value, $imp_conf['value']) !== false;
+ break;
+ case 'field-does-not-contain':
+ $item[$new_key] = strpos($previous_value, $imp_conf['value']) === false;
+ break;
+ case 'map-value':
+ $match_found = false;
+ foreach($imp_conf['map'] as $map_item) {
+ if($map_item['old'] != $previous_value) {
+ continue;
+ }
+
+ $item[$new_key] = $map_item['new'];
+ $match_found = true;
+ break;
+ }
+
+ if(!$match_found) {
+ $item[$new_key] = $previous_value;
+ }
+
+ break;
+ case 'query':
+ if(is_null($previous_value)) {
+ break;
+ }
+
+ $query = $imp_conf['query'];
+ $resRel = $old_db_connection->sendQuery(
+ 'SELECT `' . $query['field'] . '` from `' . $query['table'] . '` WHERE `' . ($query['where_field'] ?? 'id') . '` = ' . $previous_value . ' LIMIT 1;'
+ );
+
+ if($relRow = $old_db_connection->fetchArray($resRel)) {
+ $item[$new_key] = $relRow[$query['field']];
+ }
+ else {
+ $item[$new_key] = null;
+ }
+
+ break;
+ }
+
+ $previous_value = $item[$new_key];
+ }
+ }
+
+ return $item;
+};
+
+/** @var \equal\data\adapt\DataAdapter */
+$adapter = $dap->get('sql');
+
+$import_config = $getImportConfig(
+ QN_BASEDIR . '/packages/' . $params['package'] . '/init/import-config.json'
+);
+
+/** @var DBManipulator */
+$old_db_connection = $createOldDbConnection(
+ $params['db_dbms'],
+ $params['db_host'],
+ $params['db_port'],
+ $params['db_name'],
+ $params['db_user'],
+ $params['db_password'],
+ $params['db_charset'],
+ $params['db_collation']
+ );
+
+/** @var DBManipulator */
+$new_db_connection = $createNewDbConnection();
+
+$limit = 500;
+
+foreach($import_config as $config) {
+ if(isset($params['entity']) && $params['entity'] !== $config['entity']) {
+ continue;
+ }
+
+ $new_table_name = str_replace('\\', '_', $config['entity']);
+
+ $offset = 0;
+ $remaining_data = true;
+ while($remaining_data) {
+ $res = $old_db_connection->getRecords(
+ $config['old_table']['name'],
+ array_keys($config['old_table']['fields']),
+ null,
+ $config['old_table']['conditions'] ?? null,
+ $config['old_table']['id_field'] ?? 'id',
+ [],
+ $offset,
+ $limit
+ );
+
+ if($old_db_connection->getAffectedRows() < $limit) {
+ $remaining_data = false;
+ }
+
+ $items = [];
+ while ($row = $old_db_connection->fetchArray($res)) {
+ $old_item = $castOldDbRow($row, $config['old_table']['fields']);
+ $items[] = $createNewItemFromOld($config, $old_db_connection, $old_item);
+ }
+
+ if(!empty($items)) {
+ foreach($items as &$item) {
+ $item['created'] = $adapter->adaptOut($item['created'], 'datetime');
+ $item['modified'] = $adapter->adaptOut($item['modified'], 'datetime');
+ }
+
+ $new_db_connection->addRecords(
+ $new_table_name,
+ array_keys($items[0]),
+ $items
+ );
+ }
+
+ $offset += $limit;
+ }
+}
+
+$old_db_connection->disconnect();
+$new_db_connection->disconnect();
+
+$context->httpResponse()
+ ->body(['success' => true])
+ ->send();
diff --git a/packages/core/actions/init/package.php b/packages/core/actions/init/package.php
index dbe746aeb..61aedeef3 100644
--- a/packages/core/actions/init/package.php
+++ b/packages/core/actions/init/package.php
@@ -4,7 +4,7 @@
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
-use equal\db\DBConnection;
+use equal\db\DBConnector;
use equal\fs\FSManipulator as FS;
use equal\orm\Field;
@@ -54,7 +54,7 @@
eQual::run('do', 'test_db-access');
// retrieve connection object
-$db = DBConnection::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect();
+$db = DBConnector::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect();
if(!$db) {
throw new Exception('missing_database', QN_ERROR_INVALID_CONFIG);
diff --git a/packages/core/actions/model/correct.php b/packages/core/actions/model/correct.php
index 7a533bc69..d4691c949 100644
--- a/packages/core/actions/model/correct.php
+++ b/packages/core/actions/model/correct.php
@@ -52,13 +52,13 @@
// retrieve target entity
-$entity = $orm->getModel($params['entity']);
-if(!$entity) {
+$model = $orm->getModel($params['entity']);
+if(!$model) {
throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM);
}
// get the complete schema of the object (including special fields)
-$schema = $entity->getSchema();
+$schema = $model->getSchema();
// adapt received fields names for dot notation support
$fields = [];
@@ -66,7 +66,8 @@
if(!isset($schema[$field])) {
continue;
}
- $f = new Field($schema[$field]);
+ /** @var equal\orm\Field */
+ $f = $model->getField($field);
$fields[$field] = $adapter->adaptIn($value, $f->getUsage());
}
diff --git a/packages/core/actions/model/create.php b/packages/core/actions/model/create.php
index 1e6032780..f5ec2a69f 100644
--- a/packages/core/actions/model/create.php
+++ b/packages/core/actions/model/create.php
@@ -67,21 +67,25 @@
$adapter = $dap->get('json');
// fields and values have been received as a raw array : adapt received values according to schema
-$entity = $orm->getModel($params['entity']);
-if(!$entity) {
+$model = $orm->getModel($params['entity']);
+if(!$model) {
throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM);
}
-$schema = $entity->getSchema();
+$schema = $model->getSchema();
try {
foreach($params['fields'] as $field => $value) {
+ if(!isset($schema[$field])) {
+ continue;
+ }
// drop empty and unknown fields
if(is_null($value) || !isset($schema[$field])) {
unset($params['fields'][$field]);
continue;
}
- $f = new Field($schema[$field]);
+ /** @var equal\orm\Field */
+ $f = $model->getField($field);
$params['fields'][$field] = $adapter->adaptIn($value, $f->getUsage());
}
}
diff --git a/packages/core/actions/model/import.php b/packages/core/actions/model/import.php
index bd13d5393..596d4071c 100644
--- a/packages/core/actions/model/import.php
+++ b/packages/core/actions/model/import.php
@@ -101,7 +101,8 @@
foreach($class['data'] as $odata) {
foreach($odata as $field => $value) {
- $f = new Field($schema[$field]);
+ /** @var equal\orm\Field */
+ $f = $model->getField($field);
$odata[$field] = $adapter->adaptIn($value, $f->getUsage());
}
if(isset($odata['id'])) {
diff --git a/packages/core/actions/model/onchange.php b/packages/core/actions/model/onchange.php
index 98d3193c5..b108b15e2 100644
--- a/packages/core/actions/model/onchange.php
+++ b/packages/core/actions/model/onchange.php
@@ -77,7 +77,8 @@
// adapt fields in $values array
foreach($values as $field => $value) {
try {
- $f = new Field($schema[$field]);
+ /** @var equal\orm\Field */
+ $f = $model->getField($field);
// adapt received values based on their type (as defined in schema)
$values[$field] = $adapter->adaptIn($value, $f->getUsage());
}
diff --git a/packages/core/actions/model/update.php b/packages/core/actions/model/update.php
index b239831af..66164563a 100644
--- a/packages/core/actions/model/update.php
+++ b/packages/core/actions/model/update.php
@@ -79,6 +79,7 @@
// adapt received values for parameter 'fields' (which are still formatted as text)
$schema = $model->getSchema();
+
// remove unknown fields
$fields = array_filter($params['fields'], function($field) use ($schema){
return isset($schema[$field]);
@@ -86,8 +87,10 @@
ARRAY_FILTER_USE_KEY
);
-
foreach($fields as $field => $value) {
+ if(!isset($schema[$field])) {
+ continue;
+ }
$type = $schema[$field]['type'];
// drop empty fields (but allow reset to null)
if(!is_array($value) && !strlen(strval($value)) && !in_array($type, ['boolean', 'string', 'text']) && !is_null($value) ) {
@@ -96,7 +99,8 @@
}
try {
// adapt received values based on their type (as defined in schema)
- $f = new Field($schema[$field]);
+ /** @var equal\orm\Field */
+ $f = $model->getField($field);
$fields[$field] = $adapter->adaptIn($value, $f->getUsage());
}
catch(Exception $e) {
diff --git a/packages/core/actions/test/db-access.php b/packages/core/actions/test/db-access.php
index 6bcacf271..6604fddf9 100644
--- a/packages/core/actions/test/db-access.php
+++ b/packages/core/actions/test/db-access.php
@@ -4,7 +4,7 @@
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
-use equal\db\DBConnection;
+use equal\db\DBConnector;
$params = announce([
'description' => "Tests access to the database.\nIn case of success, the script simply terminates with a status code of 0 (no output).",
@@ -13,7 +13,7 @@
]);
// retrieve connection object
-$db = &DBConnection::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'));
+$db = DBConnector::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'));
// 1) test connectivity to DBMS service
$json = run('do', 'test_db-connectivity');
diff --git a/packages/core/actions/test/db-connectivity.php b/packages/core/actions/test/db-connectivity.php
index c96e0dea9..c39948f7a 100644
--- a/packages/core/actions/test/db-connectivity.php
+++ b/packages/core/actions/test/db-connectivity.php
@@ -4,16 +4,17 @@
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
-use equal\db\DBConnection;
+use equal\db\DBConnector;
$params = eQual::announce([
- 'description' => "Tests connectivity to the DBMS server.\nIn case of success, the script simply terminates with a status code of 0 (no output)",
+ 'description' => "Tests connectivity to the DBMS server.\n
+ In case of success, the script simply terminates with a status code of 0 (no output).",
'params' => [],
'constants' => ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_DBMS']
]);
// retrieve connection object
-$db = DBConnection::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'));
+$db = DBConnector::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'));
// 1) test access to DBMS service
if(!$db->canConnect()) {
diff --git a/packages/core/actions/test/package-consistency.php b/packages/core/actions/test/package-consistency.php
index ed604796c..4a6a9e8e9 100644
--- a/packages/core/actions/test/package-consistency.php
+++ b/packages/core/actions/test/package-consistency.php
@@ -4,7 +4,7 @@
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
-use equal\db\DBConnection;
+use equal\db\DBConnector;
// get listing of existing packages
$packages = eQual::run('get', 'config_packages');
@@ -369,7 +369,7 @@
// retrieve connection object
-$db = DBConnection::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect();
+$db = DBConnector::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect();
if(!$db) {
throw new Exception('missing_database', QN_ERROR_INVALID_CONFIG);
diff --git a/packages/core/actions/user/pass-update.php b/packages/core/actions/user/pass-update.php
index 45dfa1d6a..32a7806f2 100644
--- a/packages/core/actions/user/pass-update.php
+++ b/packages/core/actions/user/pass-update.php
@@ -6,7 +6,7 @@
*/
use core\User;
-list($params, $providers) = announce([
+list($params, $providers) = eQual::announce([
'description' => 'Updates the password related to a user account.',
'response' => [
'content-type' => 'application/json',
diff --git a/packages/core/apps/app/manifest.json b/packages/core/apps/app/manifest.json
index 358d9e013..be437ef45 100644
--- a/packages/core/apps/app/manifest.json
+++ b/packages/core/apps/app/manifest.json
@@ -1,6 +1,6 @@
{
"name": "app",
- "description": "Standard App to serve as surrogate for Package specific apps.",
+ "description": "Standard App to serve as surrogate for Package-specific Apps.",
"version": "1.0",
"authors": ["Cedric Francoys"],
"license": "LGPL-3",
diff --git a/packages/core/apps/app/version b/packages/core/apps/app/version
index d893fa847..bdef2bc5e 100644
--- a/packages/core/apps/app/version
+++ b/packages/core/apps/app/version
@@ -1 +1 @@
-263fca79556d705632c2bf675c2f6d3a
+774003e0a658e1d5098f0f22e7219dca
diff --git a/packages/core/apps/app/web.app b/packages/core/apps/app/web.app
index b38ac8e5f..d7c962055 100644
Binary files a/packages/core/apps/app/web.app and b/packages/core/apps/app/web.app differ
diff --git a/packages/core/apps/auth/version b/packages/core/apps/auth/version
index 2ff31b6b0..d868fbdde 100644
--- a/packages/core/apps/auth/version
+++ b/packages/core/apps/auth/version
@@ -1 +1 @@
-a382468b6c29901e622b3e275916bd91
+e78e6de9c057c992b80f8b14bb559a3f
diff --git a/packages/core/apps/auth/web.app b/packages/core/apps/auth/web.app
index 29b17d6e4..6ed78c92b 100644
Binary files a/packages/core/apps/auth/web.app and b/packages/core/apps/auth/web.app differ
diff --git a/packages/core/apps/console.php b/packages/core/apps/console.php
index dfa950939..c2ac29e29 100644
--- a/packages/core/apps/console.php
+++ b/packages/core/apps/console.php
@@ -162,7 +162,7 @@
$function = (isset($stack[$index]['function']))?$stack[$index]['function']:'';
$file = (isset($stack[$index]['file']))?$stack[$index]['file']:'';
$line = (isset($stack[$index]['line']))?$stack[$index]['line']:'';
- $text .= PHP_EOL.($i == ($n - 1))?' └ ':' ├ ';
+ $text .= PHP_EOL.(($i == ($n - 1))?' └ ':' ├ ');
$text .= "{$function} @ {$file} {$line} ";
}
}
@@ -213,7 +213,10 @@
$result[] = "-------------------------------------------------------------------------------------------------------------------------------------------";
}
}
- $result[] = $thread_display($thread);
+ foreach(explode(PHP_EOL, $thread_display($thread)) as $line) {
+ $result[] = $line;
+ }
+
$prev_thread_id = $thread['thread_id'];
$i++;
}
diff --git a/packages/core/apps/settings/version b/packages/core/apps/settings/version
index 1e00613cf..f6e9510f0 100644
--- a/packages/core/apps/settings/version
+++ b/packages/core/apps/settings/version
@@ -1 +1 @@
-a6a5979cc76c971fdbfde6cfa6ff8544
+e9c74ec2e37683536b21934e5d97e7a8
diff --git a/packages/core/apps/settings/web.app b/packages/core/apps/settings/web.app
index fbf14585c..c576339de 100644
Binary files a/packages/core/apps/settings/web.app and b/packages/core/apps/settings/web.app differ
diff --git a/packages/core/apps/welcome/index.html b/packages/core/apps/welcome/index.html
new file mode 100644
index 000000000..f5461d21f
--- /dev/null
+++ b/packages/core/apps/welcome/index.html
@@ -0,0 +1,254 @@
+
+
+
+ Welcome to eQual
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/core/apps/welcome/manifest.json b/packages/core/apps/welcome/manifest.json
index 0e34c9fe8..7ddb96e7a 100644
--- a/packages/core/apps/welcome/manifest.json
+++ b/packages/core/apps/welcome/manifest.json
@@ -6,12 +6,10 @@
"license": "LGPL-3",
"repository": "https://github.com/equalframework/apps-core-welcome.git",
"url": "/welcome",
- "icon": "home",
- "color": "#d2252c",
+ "icon": "sentiment_satisfied",
+ "color": "#2b78c3",
+ "show_in_apps": true,
"access": {
- "groups": [
- "users"
- ]
- },
- "show_in_apps": true
+ "groups": ["users"]
+ }
}
diff --git a/packages/core/apps/welcome/web.app b/packages/core/apps/welcome/web.app
index 94e8e8ac1..96cad8be5 100644
Binary files a/packages/core/apps/welcome/web.app and b/packages/core/apps/welcome/web.app differ
diff --git a/packages/core/apps/workbench/manifest.json b/packages/core/apps/workbench/manifest.json
index d661e9edd..cdb7dace7 100755
--- a/packages/core/apps/workbench/manifest.json
+++ b/packages/core/apps/workbench/manifest.json
@@ -1,12 +1,12 @@
{
"name": "Workbench",
- "description": "This is the workbench application.",
+ "description": "The workbench App allows to create, edit, and configure the components of the current installation.",
"url": "/workbench",
- "icon": "edit_note",
- "color": "#d2252c",
+ "icon": "construction",
+ "color": "#3a8f50",
"access": {
"groups": [
- "users", "admins"
+ "admins"
]
},
"show_in_apps": true
diff --git a/packages/core/apps/workbench/source b/packages/core/apps/workbench/source
index 189b60ebc..9c423378d 160000
--- a/packages/core/apps/workbench/source
+++ b/packages/core/apps/workbench/source
@@ -1 +1 @@
-Subproject commit 189b60ebc908cc21647a243055045fb78a0defb0
+Subproject commit 9c423378d4a285baedf4cd912efb1bff5176619c
diff --git a/packages/core/apps/workbench/version b/packages/core/apps/workbench/version
index ba0cfa371..fed04dee0 100644
--- a/packages/core/apps/workbench/version
+++ b/packages/core/apps/workbench/version
@@ -1 +1 @@
-5c35e5d9b0686cd9cd0b1ef66a6461e2
+767b3dd80844da5fa40bff7b615e11b0
diff --git a/packages/core/apps/workbench/web.app b/packages/core/apps/workbench/web.app
index 3a2d0c095..44d170651 100644
Binary files a/packages/core/apps/workbench/web.app and b/packages/core/apps/workbench/web.app differ
diff --git a/packages/core/classes/User.class.php b/packages/core/classes/User.class.php
index 0d5d3c38b..5cbdfcf5e 100644
--- a/packages/core/classes/User.class.php
+++ b/packages/core/classes/User.class.php
@@ -211,7 +211,7 @@ public static function calcName($self) {
foreach($self as $id => $user) {
$parts = explode(' ', str_replace('-', ' ', $user['firstname'].' '.$user['lastname']));
$initials = strtoupper(array_reduce($parts, function($c, $a) {return $c.substr($a, 0, 1);}, ''));
- $res = str_replace(['id', 'nickname', 'mail', 'givenname', 'surname', 'initials'], [$id, $user['nickname'], $user['login'], $user['firstname'], $user['lastname'], $initials], $mask);
+ $res = str_replace(['id', 'nickname', 'mail', 'firstname', 'lastname', 'initials'], [$id, $user['nickname'], $user['login'], $user['firstname'], $user['lastname'], $initials], $mask);
// fallback to user ID
$result[$id] = (strlen($res) > 0)?$res:$id;
}
@@ -230,9 +230,9 @@ public static function calcName($self) {
*/
public static function onupdatePassword($om, $ids, $values, $lang) {
$values = $om->read(self::getType(), $ids, ['password']);
- foreach($values as $oid => $odata) {
- if(substr($odata['password'], 0, 4) != '$2y$') {
- $om->update(self::getType(), $oid, ['password' => password_hash($odata['password'], PASSWORD_BCRYPT)]);
+ foreach($values as $id => $user) {
+ if(substr($user['password'], 0, 4) != '$2y$') {
+ $om->update(self::getType(), $id, ['password' => password_hash($user['password'], PASSWORD_BCRYPT)]);
}
}
}
@@ -247,7 +247,7 @@ public static function calcFullname($self) {
$result = [];
$self->read(['firstname', 'lastname']);
foreach($self as $id => $user) {
- $result[$id] = $user['firstname'].' '.$user['lastname'];
+ $result[$id] = ucfirst($user['firstname']).' '.mb_strtoupper($user['lastname']);
}
return $result;
}
@@ -278,23 +278,23 @@ public static function onchange($event, $values) {
if(isset($event['firstname'])) {
if(isset($event['lastname'])) {
- $result['fullname'] = $event['firstname'].' '.$event['lastname'];
+ $result['fullname'] = ucfirst($event['firstname']).' '.mb_strtoupper($event['lastname']);
}
else {
if(isset($values['lastname'])) {
- $result['fullname'] = $event['firstname'].' '.$values['lastname'];
+ $result['fullname'] = ucfirst($event['firstname']).' '.mb_strtoupper($values['lastname']);
}
else {
- $result['fullname'] = $event['firstname'];
+ $result['fullname'] = mb_strtoupper($event['firstname']);
}
}
}
else {
if(isset($values['firstname'])) {
- $result['fullname'] = $values['firstname'].' '.$event['lastname'];
+ $result['fullname'] = ucfirst($values['firstname']).' '.mb_strtoupper($event['lastname']);
}
else {
- $result['fullname'] = $event['lastname'];
+ $result['fullname'] = mb_strtoupper($event['lastname']);
}
}
}
diff --git a/packages/core/classes/setting/Setting.class.php b/packages/core/classes/setting/Setting.class.php
index bd1ba4fa7..2f09e346d 100644
--- a/packages/core/classes/setting/Setting.class.php
+++ b/packages/core/classes/setting/Setting.class.php
@@ -37,14 +37,16 @@ public static function getColumns() {
'type' => 'string',
'description' => 'Unique code of the parameter.',
'onupdate' => 'onupdateCode',
- 'required' => true
+ 'required' => true,
+ 'dependencies' => ['name']
],
'package' => [
'type' => 'string',
'description' => 'Package which the param refers to, if any.',
'onupdate' => 'onupdatePackage',
- 'default' => 'core'
+ 'default' => 'core',
+ 'dependencies' => ['name']
],
'section' => [
@@ -61,7 +63,8 @@ public static function getColumns() {
'foreign_object' => 'core\setting\SettingSection',
'onupdate' => 'onupdateSectionId',
'description' => 'Section the setting relates to.',
- 'required' => true
+ 'required' => true,
+ 'dependencies' => ['section', 'name']
],
'title' => [
@@ -134,7 +137,6 @@ public static function getColumns() {
}
public static function onupdateCode($om, $ids, $values, $lang) {
- $om->update(self::getType(), $ids, ['name' => null], $lang);
$settings = $om->read(self::getType(), $ids, ['setting_values_ids'], $lang);
foreach($settings as $oid => $setting) {
$om->update(SettingValue::getType(), $setting['setting_values_ids'], ['name' => null], $lang);
@@ -142,7 +144,6 @@ public static function onupdateCode($om, $ids, $values, $lang) {
}
public static function onupdateSectionId($om, $ids, $values, $lang) {
- $om->update(self::getType(), $ids, ['name' => null, 'section' => null], $lang);
$settings = $om->read(self::getType(), $ids, ['setting_values_ids'], $lang);
foreach($settings as $oid => $setting) {
$om->update(SettingValue::getType(), $setting['setting_values_ids'], ['name' => null], $lang);
@@ -150,19 +151,18 @@ public static function onupdateSectionId($om, $ids, $values, $lang) {
}
public static function onupdatePackage($om, $ids, $values, $lang) {
- $om->update(self::getType(), $ids, ['name' => null], $lang);
$settings = $om->read(self::getType(), $ids, ['setting_values_ids'], $lang);
foreach($settings as $oid => $setting) {
$om->update(SettingValue::getType(), $setting['setting_values_ids'], ['name' => null], $lang);
}
}
- public static function calcSection($om, $oids, $lang) {
+ public static function calcSection($om, $ids, $lang) {
$result = [];
- $settings = $om->read(self::getType(), $oids, ['section_id.code'], $lang);
+ $settings = $om->read(self::getType(), $ids, ['section_id.code'], $lang);
if($settings > 0 && count($settings)) {
- foreach($settings as $oid => $odata) {
- $result[$oid] = $odata['section_id.code'];
+ foreach($settings as $id => $setting) {
+ $result[$id] = $setting['section_id.code'];
}
}
return $result;
diff --git a/packages/core/classes/test/Test.class.php b/packages/core/classes/test/Test.class.php
new file mode 100644
index 000000000..8cc22a013
--- /dev/null
+++ b/packages/core/classes/test/Test.class.php
@@ -0,0 +1,50 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU GPL 3 license
+*/
+namespace core\test;
+
+use equal\orm\Model;
+
+/**
+ * @property alias $name rest
+ * @property string $login
+ * @property string $username test bonjour 1 24 5d 344 555 666
+ */
+class Test extends Model
+{
+ public static function getColumns()
+ {
+ return [
+ 'string_short' => [
+ 'type' => 'string',
+ 'usage' => 'text/plain:9',
+ 'dependents' => ['tests1_ids' => ['test']]
+ ],
+
+ 'string_currency' => [
+ 'type' => 'string',
+ 'usage' => 'currency'
+ ],
+
+ 'float_amount' => [
+ 'type' => 'float',
+ 'usage' => 'amount/money'
+ ],
+
+ 'datetime' => [
+ 'type' => 'datetime'
+ ],
+
+ 'tests1_ids' => [
+ 'type' => 'one2many',
+ 'foreign_object' => 'core\test\Test1',
+ 'foreign_field' => 'test_id'
+ ],
+
+ ];
+ }
+}
diff --git a/packages/core/classes/test/Test1.class.php b/packages/core/classes/test/Test1.class.php
new file mode 100644
index 000000000..76a0c62d3
--- /dev/null
+++ b/packages/core/classes/test/Test1.class.php
@@ -0,0 +1,41 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU GPL 3 license
+*/
+namespace core\test;
+
+use equal\orm\Model;
+
+class Test1 extends Model
+{
+ public static function getColumns()
+ {
+ return [
+ 'test' => [
+ 'type' => 'computed',
+ 'result_type' => 'string',
+ 'function' => 'calcTest',
+ 'store' => true
+ ],
+
+ 'test_id' => [
+ 'type' => 'many2one',
+ 'foreign_object' => 'core\test\Test'
+ ],
+ ];
+ }
+
+ public static function calcTest($self) {
+ $result = [];
+ $self->read(['test_id' => ['id', 'string_short']]);
+ foreach($self as $id => $test1) {
+ trigger_error("ORM::".'rel:'.$test1['test_id']['id'], QN_REPORT_WARNING);
+ trigger_error("ORM::".'val:'.$test1['test_id']['string_short'], QN_REPORT_WARNING);
+ $result[$id] = $test1['test_id']['string_short'];
+ }
+ return $result;
+ }
+}
diff --git a/packages/core/data/appinfo.php b/packages/core/data/appinfo.php
index 0e572039c..40d6c57d5 100644
--- a/packages/core/data/appinfo.php
+++ b/packages/core/data/appinfo.php
@@ -6,7 +6,7 @@
*/
list($params, $providers) = eQual::announce([
- 'description' => 'Retrieve the descriptor of a given App (from manifest), identified by package and app ID.',
+ 'description' => 'Provide the descriptor of a given App (from manifest), identified by package and app ID.',
'params' => [
'package' => [
'type' => 'string',
diff --git a/packages/core/data/config/live/routes.php b/packages/core/data/config/live/routes.php
index c2c01c5ba..0c1f0a0a2 100644
--- a/packages/core/data/config/live/routes.php
+++ b/packages/core/data/config/live/routes.php
@@ -6,6 +6,9 @@
*/
list($params, $providers) = announce([
'description' => 'Returns all the routes with priority based on the number.',
+ 'access' => [
+ 'visibility' => 'protected'
+ ],
'response' => [
'content-type' => 'application/json',
'charset' => 'UTF-8',
diff --git a/packages/core/data/config/uml.php b/packages/core/data/config/uml.php
index f4c1c88f9..4a7f07100 100644
--- a/packages/core/data/config/uml.php
+++ b/packages/core/data/config/uml.php
@@ -1,4 +1,9 @@
+ Some Rights Reserved, Cedric Francoys, 2010-2024
+ Licensed under GNU LGPL 3 license
+*/
list($params, $providers) = eQual::announce([
'description' => "Attempts to create a new package using a given name.",
@@ -9,7 +14,7 @@
'required' => true
],
'path' => [
- 'decription' => 'relative path to the file from packages/{pkg}/',
+ 'description' => 'relative path to the file from packages/{pkg}/',
'type' => 'string',
'required' => true
],
@@ -18,7 +23,7 @@
'type' => 'string',
'required' => true,
'selection' => [
- 'or'
+ 'erd'
]
]
],
@@ -61,8 +66,8 @@
$path = str_replace("..","",$path);
-if(!endsWith($filename,".{$params["type"]}.equml")) {
- $filename = $filename.".{$params["type"]}.equml";
+if(!endsWith($filename,".{$params["type"]}.json")) {
+ $filename = $filename.".{$params["type"]}.json";
}
if(!file_exists(QN_BASEDIR."/packages/{$package}/uml/{$path}/{$filename}")) {
@@ -72,7 +77,7 @@
$context->httpResponse()
->body(file_get_contents(QN_BASEDIR."/packages/{$package}/uml/{$path}/{$filename}"))
->status(200)
- ->send();
+ ->send();
function endsWith( $haystack, $needle ) {
$length = strlen( $needle );
diff --git a/packages/core/data/config/umls.php b/packages/core/data/config/umls.php
index 632dcf766..bb0995a55 100644
--- a/packages/core/data/config/umls.php
+++ b/packages/core/data/config/umls.php
@@ -1,5 +1,9 @@
+ Some Rights Reserved, Cedric Francoys, 2010-2024
+ Licensed under GNU LGPL 3 license
+*/
list($params, $providers) = eQual::announce([
'description' => 'Returns the list of menus defined in a given package, or applicable to a given entity.',
@@ -13,9 +17,9 @@
'description' => 'Type of the UML data',
'type' => 'string',
'selection' => [
- 'or'
+ 'erd'
]
- ]
+ ]
],
'providers' => ['context', 'orm']
]);
@@ -26,12 +30,12 @@
*/
list($context, $orm) = [$providers['context'], $providers['orm']];
-$packages = eQual::run('get','core_config_packages',[]);
+$packages = eQual::run('get','core_config_packages', []);
$result = [];
foreach($packages as $package) {
- $result[$package] = recurse_dir(QN_BASEDIR."/packages/{$package}/uml","equml",$params['type']);
+ $result[$package] = recurse_dir(QN_BASEDIR."/packages/{$package}/uml", "json", $params['type']);
}
$context->httpResponse()
@@ -60,7 +64,7 @@ function endsWith( $haystack, $needle ) {
}
/**
- * #memo - this method highily differs from the one in controllers.php , translations.php and menu.php
+ * #memo - this method highly differs from the one in controllers.php , translations.php and menu.php
*/
function recurse_dir($directory, $extension,$type,$parent_name='') {
$result = array();
diff --git a/packages/core/data/model/export-pdf.php b/packages/core/data/model/export-pdf.php
index 13cb641a6..2a8a08609 100644
--- a/packages/core/data/model/export-pdf.php
+++ b/packages/core/data/model/export-pdf.php
@@ -518,7 +518,12 @@ function groupObjects($schema, $objects, $group_by) {
if(is_array($key)) {
if(isset($key['name'])) {
$label = $key['name'];
- $key = $key['name'];
+ if(isset($group['order']) && isset($key[$group['order']])) {
+ $key = str_pad((string) $key[$group['order']], 11, '0', STR_PAD_LEFT);
+ }
+ else {
+ $key = $key['name'];
+ }
}
else {
$label = '';
diff --git a/packages/core/data/model/menu.php b/packages/core/data/model/menu.php
index 03c7875aa..afb30fd2f 100644
--- a/packages/core/data/model/menu.php
+++ b/packages/core/data/model/menu.php
@@ -29,7 +29,7 @@
/**
* @var \equal\php\Context $context
*/
-list($context, $orm) = [ $providers['context'] ];
+list($context) = [ $providers['context'] ];
$result = [];
diff --git a/packages/core/data/model/view.php b/packages/core/data/model/view.php
index ece41605b..010f87315 100644
--- a/packages/core/data/model/view.php
+++ b/packages/core/data/model/view.php
@@ -4,7 +4,7 @@
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU LGPL 3 license
*/
-list($params, $providers) = announce([
+list($params, $providers) = eQual::announce([
'description' => "Returns the JSON view related to an entity (class model), given a view ID ().",
'params' => [
'entity' => [
@@ -26,27 +26,151 @@
'providers' => ['context', 'orm']
]);
-
list($context, $orm) = [$providers['context'], $providers['orm']];
+$removeNodes = function (&$layout, $nodes_ids) {
+ foreach($layout['groups'] ?? [] as $group_index => $group) {
+ if(isset($group['id']) && in_array($group['id'], $nodes_ids)) {
+ array_splice($layout['groups'], $group_index, 1);
+ continue;
+ }
+ foreach($group['sections'] ?? [] as $section_index => $section) {
+ if(isset($section['id']) && in_array($section['id'], $nodes_ids)) {
+ array_splice($layout['groups'][$group_index]['sections'], $section_index, 1);
+ continue;
+ }
+ foreach($section['rows'] ?? [] as $row_index => $row) {
+ if(isset($row['id']) && in_array($row['id'], $nodes_ids)) {
+ array_splice($layout['groups'][$group_index]['sections'][$section_index]['rows'], $row_index, 1);
+ continue;
+ }
+ foreach($row['columns'] ?? [] as $column_index => $column) {
+ if(isset($column['id']) && in_array($column['id'], $nodes_ids)) {
+ array_splice($layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index]['columns'], $column_index, 1);
+ continue;
+ }
+ foreach($row['items'] ?? [] as $item_index => $item) {
+ if(isset($item['id']) && in_array($item['id'], $nodes_ids)) {
+ array_splice($layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index]['columns'][$column_index]['items'], $item_index, 1);
+ continue;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ foreach($layout['items'] ?? [] as $item_index => $item) {
+ if(isset($item['id']) && in_array($item['id'], $nodes_ids)) {
+ array_splice($layout['items'], $item_index, 1);
+ }
+ }
+};
+
+$updateNode = function (&$layout, $id, $node) {
+ $target = null;
+ $index = 0;
+ $target_parent = null;
+ foreach($layout['groups'] as $group_index => $group) {
+ if(isset($group['id']) && $group['id'] == $id) {
+ $target = &$layout['groups'][$group_index];
+ break;
+ }
+ $target_parent = &$layout['groups'][$group_index]['sections'];
+ foreach($group['sections'] as $section_index => $section) {
+ if(isset($section['id']) && $section['id'] == $id) {
+ $target = &$layout['groups'][$group_index]['sections'][$section_index];
+ $index = $section_index;
+ break 2;
+ }
+ $target_parent = &$layout['groups'][$group_index]['sections'][$section_index]['rows'];
+ foreach($section['rows'] as $row_index => $row) {
+ if(isset($row['id']) && $row['id'] == $id) {
+ $target = &$layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index];
+ $index = $row_index;
+ break 3;
+ }
+ $target_parent = &$layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index]['columns'];
+ foreach($row['columns'] as $column_index => $column) {
+ if(isset($column['id']) && $column['id'] == $id) {
+ $target = &$layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index]['columns'][$column_index];
+ $index = $column_index;
+ break 4;
+ }
+ $target_parent = &$layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index]['columns'][$column_index]['items'];
+ foreach($column['items'] as $item_index => $item) {
+ if(isset($item['id']) && $item['id'] == $id) {
+ $target = &$layout['groups'][$group_index]['sections'][$section_index]['rows'][$row_index]['columns'][$column_index]['items'][$item_index];
+ $index = $item_index;
+ break 5;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if(isset($layout['items'])) {
+ $target_parent = &$layout['items'];
+ foreach($layout['items'] as $item_index => $item) {
+ if(isset($item['id']) && $item['id'] == $id) {
+ $target = &$layout['items'][$item_index];
+ $index = $item_index;
+ }
+ }
+ }
+
+ if($target) {
+ if(isset($node['attributes'])) {
+ foreach((array) $node['attributes'] as $attribute => $value) {
+ $target[$attribute] = $value;
+ }
+ }
+ if(isset($node['prepend'])) {
+ foreach((array) $node['prepend'] as $elem) {
+ array_unshift($target, $elem);
+ }
+ }
+ if(isset($node['append'])) {
+ foreach((array) $node['append'] as $elem) {
+ array_push($target, $elem);
+ }
+ }
+ if($target_parent) {
+ if(isset($node['before'])) {
+ array_splice($target_parent, $index, 0, (array) $node['before']);
+ $index += count((array) $node['before']);
+ }
+ if(isset($node['after'])) {
+ array_splice($target_parent, $index + 1, 0, (array) $node['after']);
+ }
+ }
+ }
+};
+
+
$entity = $params['entity'];
list($view_type, $view_name) = explode('.', $params['view_id']);
-// retrieve existing view meant for entity (recurse through parents)
+if(!in_array($view_type, ['form', 'list', 'chart', 'search', 'report', 'cards', 'dashboard'])) {
+ throw new Exception('invalid_view_type', EQ_ERROR_INVALID_PARAM);
+}
+
+// pass-1 : retrieve existing view meant for entity (recurse through parents)
while(true) {
$parts = explode('\\', $entity);
$package = array_shift($parts);
- $file = array_pop($parts);
+ $filename = array_pop($parts);
$class_path = implode('/', $parts);
- $file = QN_BASEDIR."/packages/{$package}/views/{$class_path}/{$file}.{$view_type}.{$view_name}.json";
+ $file = QN_BASEDIR."/packages/{$package}/views/{$class_path}/{$filename}.{$view_type}.{$view_name}.json";
if(file_exists($file)) {
break;
}
// fallback to default variant of the view
- $file = QN_BASEDIR."/packages/{$package}/views/{$class_path}/{$file}.{$view_type}.default.json";
+ $file = QN_BASEDIR."/packages/{$package}/views/{$class_path}/{$filename}.{$view_type}.default.json";
if(file_exists($file)) {
break;
}
@@ -69,10 +193,36 @@
throw new Exception("missing_view", QN_ERROR_UNKNOWN_OBJECT);
}
-if( ($view = json_decode(@file_get_contents($file), true)) === null) {
+if(($view = json_decode(@file_get_contents($file), true)) === null) {
throw new Exception("malformed_view_schema", QN_ERROR_INVALID_CONFIG);
}
+if(!isset($view['layout'])) {
+ throw new Exception("malformed_view_schema", QN_ERROR_INVALID_CONFIG);
+}
+
+// pass-2 : adapt the view if inheritance is involved
+if(isset($view['layout']['extends'])) {
+ if(!isset($view['layout']['extends']['view'])) {
+ throw new Exception("malformed_view_schema", QN_ERROR_INVALID_CONFIG);
+ }
+ $view_id = $view['layout']['extends']['view'];
+ $entity = $view['layout']['extends']['entity'] ?? $params['entity'];
+ if($params['view_id'] == $view_id && $params['entity'] == $entity) {
+ throw new Exception("cyclic_view_dependency", QN_ERROR_INVALID_CONFIG);
+ }
+ $parent_view = eQual::run('get', 'model_view', ['entity' => $entity, 'view_id' => $view_id]);
+ if(isset($view['layout']['remove'])) {
+ $removeNodes($parent_view['layout'], (array) $view['layout']['remove']);
+ }
+ if(isset($view['layout']['update'])) {
+ foreach((array) $view['layout']['update'] as $id => $node) {
+ $updateNode($parent_view['layout'], $id, $node);
+ }
+ }
+ $view['layout'] = $parent_view['layout'];
+}
+
$context->httpResponse()
->body($view)
->send();
diff --git a/packages/core/data/utils/sql-schema.php b/packages/core/data/utils/sql-schema.php
index b17e3a815..268bc291d 100644
--- a/packages/core/data/utils/sql-schema.php
+++ b/packages/core/data/utils/sql-schema.php
@@ -5,7 +5,7 @@
Licensed under GNU LGPL 3 license
*/
use equal\orm\ObjectManager;
-use equal\db\DBConnection;
+use equal\db\DBConnector;
// get listing of existing packages
$packages = eQual::run('get', 'config_packages');
@@ -49,7 +49,7 @@
exit(1);
}
// retrieve connection object
-$db = DBConnection::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect();
+$db = DBConnector::getInstance(constant('DB_HOST'), constant('DB_PORT'), constant('DB_NAME'), constant('DB_USER'), constant('DB_PASSWORD'), constant('DB_DBMS'))->connect();
if(!$db) {
throw new Exception('missing_database', QN_ERROR_INVALID_CONFIG);
diff --git a/packages/core/i18n/es/User.json b/packages/core/i18n/es/User.json
index ab3b94287..132e2b36b 100644
--- a/packages/core/i18n/es/User.json
+++ b/packages/core/i18n/es/User.json
@@ -112,8 +112,8 @@
"group.user": {
"label": "Información del usuario"
},
- "user_details": {
- "label": "Detalles del usuario"
+ "section.details": {
+ "label": "Detalles"
},
"section.preferences": {
"label": "Preferencias"
diff --git a/packages/core/i18n/fr/User.json b/packages/core/i18n/fr/User.json
index 53ed8ec22..6f14cade2 100644
--- a/packages/core/i18n/fr/User.json
+++ b/packages/core/i18n/fr/User.json
@@ -41,8 +41,8 @@
"label.details":{
"label":"Détails de contact"
},
- "section.user_details": {
- "label": "Section des détails"
+ "section.details": {
+ "label": "Détails"
},
"identification":{
"label":"Identification"
diff --git a/packages/core/i18n/fr/locale.json b/packages/core/i18n/fr/locale.json
index 49bfd4e3f..fad7d3be7 100644
--- a/packages/core/i18n/fr/locale.json
+++ b/packages/core/i18n/fr/locale.json
@@ -47,9 +47,13 @@
"numbers.decimal_separator": ",",
"currency.symbol_position": "after",
"currency.symbol_separator": " ",
+ "date.month.full": "MMMM YYYY",
+ "date.month.long": "MMM YYYY",
+ "date.month.medium": "MMM YY",
+ "date.month.short": "MM/YY",
"date.short.day": "ddd DD/MM/YY",
"date.short": "DD/MM/YY",
- "date.medium": "DD/MMM/YYYY",
+ "date.medium": "DD/MM/YYYY",
"date.long": "ddd DD MMM YYYY",
"date.full": "dddd DD MMMM YYYY",
"time.short": "HH:mm",
@@ -57,7 +61,7 @@
"time.long": "HH:mm:ss",
"time.full": "HH:mm:ss.SSS",
"datetime.short": "DD/MM/YY HH:mm",
- "datetime.medium": "DD/MMM/YYYY HH:mm",
+ "datetime.medium": "DD/MM/YYYY HH:mm",
"datetime.long": "ddd DD MMM YYYY HH:mm",
"datetime.full": "dddd DD MMMM YYYY HH:mm"
}
diff --git a/packages/core/init/data/core_User.json b/packages/core/init/data/core_User.json
index 34b0720e6..ed2ac1fc9 100644
--- a/packages/core/init/data/core_User.json
+++ b/packages/core/init/data/core_User.json
@@ -5,20 +5,20 @@
"data": [
{
"id": 1,
- "login": "root@host.local",
+ "login": "root@equal.local",
"password": "secure_password",
- "firstname": "root",
- "lastname": "@system",
+ "firstname": "Root",
+ "lastname": "USER",
"language": "en",
"validated": true,
"groups_ids": [1, 2]
},
{
"id": 2,
- "login": "cedric@equal.run",
+ "login": "user@equal.local",
"password": "safe_pass",
- "firstname": "Cédric",
- "lastname": "Françoys",
+ "firstname": "First",
+ "lastname": "USER",
"language": "en",
"validated": true,
"groups_ids": [2]
diff --git a/packages/core/manifest.json b/packages/core/manifest.json
index f3d2d2050..70bab8f2c 100644
--- a/packages/core/manifest.json
+++ b/packages/core/manifest.json
@@ -1,10 +1,15 @@
{
"name": "core",
- "description": "Foundations package holding the application logic of the elementary entities.",
+ "description": "Foundation package holding common application logic and elementary entities.",
"version": "2.0",
"authors": ["Cedric Francoys"],
"license": "LGPL-3",
+ "tags": [ "equal", "core" ],
"depends_on": [],
- "apps": [ "apps", "auth", "app", "settings","workbench" ],
- "tags": ["equal", "core"]
-}
\ No newline at end of file
+ "requires": {
+ "swiftmailer/swiftmailer": "^6.2",
+ "phpoffice/phpspreadsheet": "^1.4",
+ "dompdf/dompdf": "^0.8.3"
+ },
+ "apps": [ "apps", "auth", "app", "settings", "workbench", "welcome" ]
+}
diff --git a/packages/core/tests/access.php b/packages/core/tests/access.php
index 433a46ef5..297a6b0e4 100644
--- a/packages/core/tests/access.php
+++ b/packages/core/tests/access.php
@@ -1,6 +1,6 @@
+ This file is part of the eQual framework
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
@@ -58,7 +58,7 @@
// groups assignments
'0201' => [
- 'description' => "Verify assignment with non-existing group.",
+ 'description' => "Verify assignment with non-existing group.",
'help' => "Create a user, check with a non existing group.",
'return' => ['integer'],
'arrange' => function() use($providers) {
@@ -82,7 +82,7 @@
'help' => "Create a user, check with a non existing group.",
'return' => ['integer'],
'arrange' => function() use($providers) {
- $user = User::create(['login' => 'user_test_1@example.com', 'password' => 'abcd1234'])->first();
+ $user = User::create(['login' => 'user_test_2@example.com', 'password' => 'abcd1234'])->first();
return $user['id'];
},
'act' => function($user_id) use($providers) {
@@ -93,7 +93,7 @@
return ($access->hasGroup('users', $user_id) && !$access->hasGroup('foo_non_existing_group', $user_id));
},
'rollback' => function() {
- User::search(['login', '=', 'user_test_1@example.com'])->delete(true);
+ User::search(['login', '=', 'user_test_2@example.com'])->delete(true);
}
],
@@ -102,7 +102,7 @@
'help' => "Create a user, assign it to a group, and check the group membership of the user.",
'return' => ['array'],
'arrange' => function() use($providers) {
- $user = User::create(['login' => 'user_test_1@example.com', 'password' => 'abcd1234'])->first();
+ $user = User::create(['login' => 'user_test_3@example.com', 'password' => 'abcd1234'])->first();
$group = Group::create(['name' => 'test1'])->first();
return [$user['id'], $group['id']];
},
@@ -120,7 +120,7 @@
},
'rollback' => function() {
Group::search(['name', '=', 'test1'])->delete(true);
- User::search(['login', '=', 'user_test_1@example.com'])->delete(true);
+ User::search(['login', '=', 'user_test_3@example.com'])->delete(true);
}
],
@@ -155,7 +155,7 @@
'description' => "Check if a user has a right on all objects.",
'help' => "Create a user, assign it to a group, grant some rights to that group and check the resulting rights of the user.",
'arrange' => function() use($providers) {
- $user = User::create(['login' => 'user_test_1@example.com', 'password' => 'abcd1234'])->first();
+ $user = User::create(['login' => 'user_test_4@example.com', 'password' => 'abcd1234'])->first();
return $user['id'];
},
'assert' => function($user_id) use($providers) {
@@ -163,7 +163,7 @@
return !$access->hasRight($user_id, EQ_R_MANAGE, 'core\User');
},
'rollback' => function() {
- User::search(['login', '=', 'user_test_1@example.com'])->delete(true);
+ User::search(['login', '=', 'user_test_4@example.com'])->delete(true);
}
],
@@ -172,8 +172,8 @@
'help' => "Create a user, assign it to a group, grant some rights to that group and check the resulting rights of the user.",
'arrange' => function() use($providers) {
$access = $providers['access'];
- $user = User::create(['login' => 'user_test_1@example.com', 'password' => 'abcd1234'])->first();
- $group = Group::create(['name' => 'test1'])->first();
+ $user = User::create(['login' => 'user_test_5@example.com', 'password' => 'abcd1234'])->first();
+ $group = Group::create(['name' => 'test2'])->first();
$access->addGroup($group['id'], $user['id']);
return $group['id'];
},
@@ -188,10 +188,9 @@
return $access->hasRight($user['id'], EQ_R_READ|EQ_R_WRITE|EQ_R_MANAGE, '*');
},
'rollback' => function() {
- Group::search(['name', '=', 'test1'])->delete(true);
- User::search(['login', '=', 'user_test_1@example.com'])->delete(true);
+ Group::search(['name', '=', 'test2'])->delete(true);
+ User::search(['login', '=', 'user_test_5@example.com'])->delete(true);
}
-
],
'0305' => [
@@ -199,8 +198,8 @@
'help' => "Create a user, assign it to a group, grant some rights to that group, then remove back those rights, and check the resulting rights of the user.",
'arrange' => function() use($providers) {
$access = $providers['access'];
- $user = User::create(['login' => 'user_test_1@example.com', 'password' => 'abcd1234'])->first();
- $group = Group::create(['name' => 'test1'])->first();
+ $user = User::create(['login' => 'user_test_6@example.com', 'password' => 'abcd1234'])->first();
+ $group = Group::create(['name' => 'test3'])->first();
$access->grantGroups($group['id'], EQ_R_MANAGE, '*');
$access->addGroup($group['id'], $user['id']);
return [$user['id'], $group['id']];
@@ -217,8 +216,8 @@
return !$access->hasRight($user_id, EQ_R_MANAGE, 'core\User', 1);
},
'rollback' => function() {
- Group::search(['name', '=', 'test1'])->delete(true);
- User::search(['login', '=', 'user_test_1@example.com'])->delete(true);
+ Group::search(['name', '=', 'test3'])->delete(true);
+ User::search(['login', '=', 'user_test_6@example.com'])->delete(true);
}
],
@@ -227,7 +226,7 @@
'description' => "Check if an action is authorized.",
'help' => "Create a user, and check if the user can perform an action on its own object.",
'arrange' => function() use($providers) {
- $user = User::create(['login' => 'user_test_1@example.com', 'password' => 'abcd1234'])->first();
+ $user = User::create(['login' => 'user_test_7@example.com', 'password' => 'abcd1234'])->first();
return $user['id'];
},
'assert' => function($user_id) use($providers) {
@@ -235,7 +234,7 @@
return !boolval(count($access->canPerform($user_id, 'validate', 'core\User', $user_id)));
},
'rollback' => function() {
- User::search(['login', '=', 'user_test_1@example.com'])->delete(true);
+ User::search(['login', '=', 'user_test_7@example.com'])->delete(true);
}
]
];
diff --git a/packages/core/tests/adapters.php b/packages/core/tests/adapters.php
index 0b4231d28..3b084213e 100644
--- a/packages/core/tests/adapters.php
+++ b/packages/core/tests/adapters.php
@@ -1,6 +1,6 @@
+ This file is part of the eQual framework
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
diff --git a/packages/core/tests/auth.php b/packages/core/tests/auth.php
index 040944d41..10fafe190 100644
--- a/packages/core/tests/auth.php
+++ b/packages/core/tests/auth.php
@@ -1,6 +1,6 @@
+ This file is part of the eQual framework
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
diff --git a/packages/core/tests/collections.php b/packages/core/tests/collections.php
new file mode 100644
index 000000000..55eaf1e75
--- /dev/null
+++ b/packages/core/tests/collections.php
@@ -0,0 +1,44 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU GPL 3 license
+*/
+use equal\orm\ObjectManager;
+use equal\http\HttpRequest;
+use core\User;
+use core\Group;
+
+$providers = eQual::inject(['context', 'orm', 'auth', 'access']);
+
+$tests = [
+ '40101' => [
+ 'description' => "Retrieve sub-object using dot notation with ORM::read (with recursion).",
+ 'act' => function () use ($providers) {
+ $orm = $providers['orm'];
+ $res = $orm->read('core\User', QN_ROOT_USER_ID, ['name', 'groups_ids.name', 'groups_ids.id', 'groups_ids.users_ids.name']);
+ return ($res > 0 && count($res))?reset($res):[];
+ },
+ 'assert' => function($result) {
+ $res = [];
+ foreach($result['groups_ids.name'] as $gid => $group) {
+ if(!isset($res[$gid])) {
+ $res[$gid] = [];
+ }
+ $res[$gid]['name'] = $group['name'];
+ }
+ foreach($result['groups_ids.users_ids.name'] as $gid => $group) {
+ if(!isset($res[$gid])) {
+ $res[$gid] = [];
+ }
+ foreach($group['users_ids.name'] as $uid => $user) {
+ if(!isset($res[$gid]['users_ids'])) {
+ $res[$gid]['users_ids'] = [];
+ }
+ $res[$gid]['users_ids'][$uid] = $user['name'];
+ }
+ }
+ return ($res[1]['name'] == 'admins' && $res[2]['users_ids'][1] == 'root@equal.local');
+ }
+ ]
+];
\ No newline at end of file
diff --git a/packages/core/tests/computed.php b/packages/core/tests/computed.php
new file mode 100644
index 000000000..cb6acac84
--- /dev/null
+++ b/packages/core/tests/computed.php
@@ -0,0 +1,27 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU GPL 3 license
+*/
+
+$tests = [
+
+ '1001' => [
+ 'description' => "Checking dependents chaining with computed field of related object.",
+ 'act' => function () {
+ $result = [];
+ $test = core\test\Test::create(['string_short' => 'test 0'])->read(['id'])->first();
+ $test1 = core\test\Test1::create(['test_id' => $test['id']])->read(['id', 'test'])->first();
+ $result[] = $test1['test'];
+ core\test\Test::id($test['id'])->update(['string_short' => 'test 1']);
+ $test1 = core\test\Test1::id($test1['id'])->read(['test'])->first();
+ $result[] = $test1['test'];
+ return $result;
+ },
+ 'assert' => function($result) {
+ return ($result[0] == 'test 0' && $result[1] == 'test 1');
+ }
+ ]
+
+];
\ No newline at end of file
diff --git a/packages/core/tests/cron.php b/packages/core/tests/cron.php
index fa16362d7..b5a58f068 100644
--- a/packages/core/tests/cron.php
+++ b/packages/core/tests/cron.php
@@ -1,6 +1,6 @@
+ This file is part of the eQual framework
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
diff --git a/packages/core/tests/demo.php b/packages/core/tests/demo.php
index 958e904be..19399c863 100644
--- a/packages/core/tests/demo.php
+++ b/packages/core/tests/demo.php
@@ -1,17 +1,15 @@
+ This file is part of the eQual framework
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
/**
- * A global var `$test` is expected to be set by each tests set (that var is used in the `core_package_test` controller).
- * As the current file is injected in the global scope, this line is not mandatory and is left in order to ease the understanding.
- *
- * @var array $test Global var holding the test descriptors.
+ * A `$test` var is expected to be set by each tests set.
+ * That var is used in the `core_test_package` controller and, since tests sets are loaded using `include($filename)`,
+ * the `$tests` var is shared between tests sets and the parent script.
*/
-global $test;
$tests = [
// Each key of the associative array is a test identifier that maps to a test descriptor.
diff --git a/packages/core/tests/http.php b/packages/core/tests/http.php
index c7f1a151e..dec5d6239 100644
--- a/packages/core/tests/http.php
+++ b/packages/core/tests/http.php
@@ -1,6 +1,6 @@
+ This file is part of the eQual framework
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
@@ -23,7 +23,7 @@
try {
$request = new HttpRequest("http://localhost/me");
$response = $request
- ->header('Authorization', 'Basic '.base64_encode("cedric@equal.run:safe_pass"))
+ ->header('Authorization', 'Basic '.base64_encode("user@equal.local:safe_pass"))
->send();
return $response->body();
}
@@ -33,7 +33,7 @@
}
return $values;
},
- 'expected' => ['id' => 2, 'login' => 'cedric@equal.run', 'firstname' => 'Cédric', 'lastname' => 'FRANÇOYS', 'language' => 'fr']
+ 'expected' => ['id' => 2, 'login' => 'user@equal.local', 'firstname' => 'User', 'lastname' => 'USER', 'language' => 'fr']
),
*/
];
\ No newline at end of file
diff --git a/packages/core/tests/orm.php b/packages/core/tests/orm.php
index cb11cef09..3a5fbd9b1 100644
--- a/packages/core/tests/orm.php
+++ b/packages/core/tests/orm.php
@@ -1,6 +1,6 @@
+ This file is part of the eQual framework
Some Rights Reserved, Cedric Francoys, 2010-2021
Licensed under GNU GPL 3 license
*/
@@ -17,7 +17,7 @@
//1xxx : calls related to the ObjectManger instance
'1000' => array(
- 'description' => "Get instance of the object Manager",
+ 'description' => "Get the instance of the object Manager.",
'return' => array('boolean'),
'expected' => true,
'test' => function (){
@@ -27,7 +27,7 @@
),
'1100' => array(
- 'description' => "Check uniqueness of ObjectManager instance",
+ 'description' => "Check uniqueness of ObjectManager instance.",
'return' => array('boolean'),
'expected' => true,
'test' => function (){
@@ -42,13 +42,13 @@
// @return mixed (int or array) error code OR resulting associative array
'2100' => array(
- 'description' => "Requesting User object by passing an array holding a unique id",
+ 'description' => "Requesting User object by passing an array holding a unique id.",
'return' => array('integer', 'array'),
'expected' => array(
'1' => array(
'language' => 'en',
- 'firstname' => 'root',
- 'lastname' => '@system'
+ 'firstname' => 'Root',
+ 'lastname' => 'USER'
)
),
'test' => function (){
@@ -63,13 +63,13 @@
),
'2101' => array(
- 'description' => "Requesting User object by passing an integer as id",
+ 'description' => "Requesting User object by passing an integer as id.",
'return' => array('integer', 'array'),
'expected' => array(
'1' => array(
'language' => 'en',
- 'firstname' => 'root',
- 'lastname' => '@system'
+ 'firstname' => 'Root',
+ 'lastname' => 'USER'
)
),
'test' => function (){
@@ -83,13 +83,13 @@
}
),
'2102' => array(
- 'description' => "Requesting User object by passing a string as id",
+ 'description' => "Requesting User object by passing a string as id.",
'return' => array('integer', 'array'),
'expected' => array(
'1' => array(
'language' => 'en',
- 'firstname' => 'root',
- 'lastname' => '@system'
+ 'firstname' => 'Root',
+ 'lastname' => 'USER'
)
),
'test' => function (){
@@ -104,7 +104,7 @@
),
'2103' => array(
- 'description' => "Requesting User object by giving a non-existing integer id",
+ 'description' => "Requesting User object by giving a non-existing integer id.",
'return' => array('integer', 'array'),
'expected' => array(),
'test' => function (){
@@ -114,13 +114,13 @@
),
'2104' => array(
- 'description' => "Requesting User object by passing an array containing an invalid id",
+ 'description' => "Requesting User object by passing an array containing an invalid id.",
'return' => array('integer', 'array'),
'expected' => array(
'1' => [
'language' => 'en',
- 'firstname' => 'root',
- 'lastname' => '@system'
+ 'firstname' => 'Root',
+ 'lastname' => 'USER'
]
),
'test' => function () {
@@ -131,12 +131,11 @@
$res[$oid] = $object->toArray();
}
return $res;
-
}
),
'2105' => array(
- 'description' => "Call ObjectManager::read with empty value for \$ids parameter : empty array",
+ 'description' => "Call ObjectManager::read with empty value for \$ids parameter : empty array.",
'return' => array('integer', 'array'),
'expected' => array(),
'test' => function () {
@@ -146,7 +145,7 @@
),
'2110' => array(
- 'description' => "Call ObjectManager::read with missing \$ids parameters",
+ 'description' => "Call ObjectManager::read with missing \$ids parameters.",
'return' => array('integer', 'array'),
'expected' => [],
'test' => function () {
@@ -155,7 +154,7 @@
}
),
'2120' => array(
- 'description' => "Call ObjectManager::read with wrong \$ids parameters",
+ 'description' => "Call ObjectManager::read with wrong \$ids parameters.",
'return' => array('integer', 'array'),
'expected' => array(),
'test' => function () {
@@ -164,7 +163,7 @@
}
),
'2130' => array(
- 'description' => "Call ObjectManager::read some non-existing object from non-existing class",
+ 'description' => "Call ObjectManager::read some non-existing object from non-existing class.",
'return' => array('integer', 'array'),
'expected' => QN_ERROR_UNKNOWN_OBJECT,
'test' => function () {
@@ -174,11 +173,11 @@
),
'2140' => array(
- 'description' => "Call ObjectManager::read with a string as field",
+ 'description' => "Call ObjectManager::read with a string as field.",
'return' => array('integer', 'array'),
'expected' => array(
'1' => array(
- 'firstname' => 'root'
+ 'firstname' => 'Root'
)
),
'test' => function () {
@@ -192,7 +191,7 @@
}
),
'2150' => array(
- 'description' => "Call ObjectManager::read with wrong \$fields value : non-existing field name",
+ 'description' => "Call ObjectManager::read with wrong \$fields value : non-existing field name.",
'return' => array('integer', 'array'),
'expected' => [
'1' => []
@@ -208,9 +207,9 @@
}
),
'2151' => array(
- 'description' => "Call ObjectManager::read with wrong \$fields value : non-existing field name",
+ 'description' => "Call ObjectManager::read with wrong \$fields value : non-existing field name.",
'return' => array('integer', 'array'),
- 'expected' => array('1' => array('firstname' => 'root') ),
+ 'expected' => array('1' => array('firstname' => 'Root') ),
'test' => function () {
$res = [];
$om = ObjectManager::getInstance();
@@ -225,7 +224,7 @@
//22xx : calls related to the create method
'2210' => array(
- 'description' => "Create a user (no validation)",
+ 'description' => "Create a user (no validation).",
'return' => array('integer'),
'test' => function () {
global $dummy_user_id;
@@ -241,7 +240,7 @@
),
'2220' => [
- 'description' => "Create a group (no validation)",
+ 'description' => "Create a group (no validation).",
'return' => array('integer'),
'act' => function () {
$om = ObjectManager::getInstance();
@@ -263,7 +262,7 @@
//24xx : calls related to the remove method
'2401' => array(
- 'description' => "Remove a user (no validation)",
+ 'description' => "Remove a user (no validation).",
'return' => array('integer', 'array'),
'assert' => function($result) {
return ($result > 0);
@@ -279,7 +278,7 @@
// @signature : public function search($object_class, $domain=NULL, $order='id', $sort='asc', $start='0', $limit='0', $lang='en') {
// @return : mixed (integer or array)
'2501' => array(
- 'description' => "Search an object with valid clause 'ilike'",
+ 'description' => "Search an object with valid clause 'ilike'.",
'return' => array('integer', 'array'),
'expected' => array('2'),
'test' => function () {
@@ -288,7 +287,7 @@
}
),
'2502' => array(
- 'description' => "Search an object with invalid clause 'ilike' (non-existing field)",
+ 'description' => "Search an object with invalid clause 'ilike' (non-existing field).",
'return' => array('integer', 'array'),
'expected' => QN_ERROR_INVALID_PARAM,
'test' => function () {
@@ -297,7 +296,7 @@
}
),
'2510' => array(
- 'description' => "Search for some object : clause 'contains' on one2many field",
+ 'description' => "Search for some object : clause 'contains' on one2many field.",
'return' => array('boolean'),
'expected' => true,
'test' => function (){
@@ -306,7 +305,7 @@
},
),
'2520' => array(
- 'description' => "Search for some object : clause 'contains' on one2many field (using a foreign key different from 'id')",
+ 'description' => "Search for some object : clause 'contains' on one2many field (using a foreign key different from 'id').",
'return' => array('boolean'),
'expected' => true,
'test' => function () {
@@ -315,7 +314,7 @@
}
),
'2530' => array(
- 'description' => "Search for some object : clause 'contains' on many2one field",
+ 'description' => "Search for some object : clause 'contains' on many2one field.",
'return' => array('boolean'),
'expected' => true,
'test' => function () {
@@ -331,7 +330,7 @@
'expected' => QN_ROOT_USER_ID,
'test' => function () use($providers) {
try {
- $providers['auth']->authenticate('root@host.local', 'secure_password');
+ $providers['auth']->authenticate('root@equal.local', 'secure_password');
$values = $providers['auth']->userId();
}
catch(Exception $e) {
@@ -343,11 +342,11 @@
),
'2620' => array(
- 'description' => "Search for some object : clause 'contains' on many2many field",
+ 'description' => "Search for some object : clause 'contains' on many2many field.",
'return' => array('integer', 'array'),
'arrange' => function () use($providers) {
try {
- $providers['auth']->authenticate('cedric@equal.run', 'safe_pass');
+ $providers['auth']->authenticate('user@equal.local', 'safe_pass');
// grant READ operation on all classes
$providers['access']->grant(QN_R_READ);
@@ -363,14 +362,14 @@
},
'assert' => function($result) {
return is_array($result) && count($result) == 2 && (
- count(array_diff(['id' => 1, 'login' => 'root@host.local'], (array) $result['1'])) == 0
- && count(array_diff(['id' => 2, 'login' => 'cedric@equal.run'], (array) $result['2'])) == 0
+ count(array_diff(['id' => 1, 'login' => 'root@equal.local'], (array) $result['1'])) == 0
+ && count(array_diff(['id' => 2, 'login' => 'user@equal.local'], (array) $result['2'])) == 0
);
}
),
'2631' => array(
- 'description' => "Add a user to a given group",
+ 'description' => "Add a user to a given group.",
'return' => array('integer', 'array'),
'act' => function () use($providers) {
try {
@@ -389,7 +388,7 @@
},
'assert' => function($result) {
return (
- count(array_diff(['id' => 1, 'login' => 'root@host.local'], (array) $result['1'])) == 0
+ count(array_diff(['id' => 1, 'login' => 'root@equal.local'], (array) $result['1'])) == 0
);
}
),
@@ -416,7 +415,7 @@
'return' => array('integer', 'array'),
'act' => function () {
try {
- $values = User::search(['login', 'like', 'cedric@equal.run'])
+ $values = User::search(['login', 'like', 'user@equal.local'])
->read(['login'])
->get();
}
@@ -429,7 +428,7 @@
'assert' => function($result) {
return (
count($result) &&
- count(array_diff(['id' => 2, 'login' => 'cedric@equal.run'], (array) $result[2])) == 0
+ count(array_diff(['id' => 2, 'login' => 'user@equal.local'], (array) $result[2])) == 0
);
}
),
@@ -438,7 +437,7 @@
'return' => array('integer', 'array'),
'act' => function () {
try {
- $values = User::search(['login', '=', 'cedric@equal.run'])
+ $values = User::search(['login', '=', 'user@equal.local'])
->read(['login'])
->get(true);
}
@@ -451,7 +450,7 @@
'assert' => function($result) {
return (
count($result) &&
- count(array_diff(['id' => 2, 'login' => 'cedric@equal.run'], (array) $result[0])) == 0
+ count(array_diff(['id' => 2, 'login' => 'user@equal.local'], (array) $result[0])) == 0
);
}
),
diff --git a/packages/core/tests/validation.php b/packages/core/tests/validation.php
new file mode 100644
index 000000000..b54d3647b
--- /dev/null
+++ b/packages/core/tests/validation.php
@@ -0,0 +1,210 @@
+
+ Some Rights Reserved, Cedric Francoys, 2010-2021
+ Licensed under GNU GPL 3 license
+*/
+use core\User;
+
+$tests = [
+
+ '1001' => [
+ 'description' => "Checking `User::getConstraints()` 'username' validation rule with invalid value.",
+ 'act' => function () {
+ $result = 0;
+ try {
+ User::id(1)->update(['username' => '-invalid_name-']);
+ }
+ catch(Exception $e) {
+ $result = $e->getCode();
+ }
+ return $result;
+ },
+ 'assert' => function($result) {
+ return ($result == EQ_ERROR_INVALID_PARAM);
+ }
+ ],
+ '1002' => [
+ 'description' => "Checking `User::getConstraints()` 'username' validation rule with valid value.",
+ 'act' => function () {
+ $result = 0;
+ try {
+ User::id(1)->update(['username' => 'valid-name']);
+ }
+ catch(Exception $e) {
+ $result = $e->getCode();
+ }
+ return $result;
+ },
+ 'assert' => function($result) {
+ return ($result == 0);
+ },
+ 'rollback' => function() {
+ User::id(1)->update(['username' => null]);
+ }
+ ],
+ '1011' => [
+ 'description' => "Checking usage validation 'text/plain:9' with valid value.",
+ 'act' => function () {
+ $result = 0;
+ try {
+ core\test\Test::create(['string_short' => '123456789']);
+ }
+ catch(Exception $e) {
+ $result = $e->getCode();
+ }
+ return $result;
+ },
+ 'assert' => function($result) {
+ return ($result == 0);
+ }
+ ],
+ '1012' => [
+ 'description' => "Checking usage validation 'text/plain:9' with invalid (size overflow).",
+ 'act' => function () {
+ $result = 0;
+ try {
+ core\test\Test::create(['string_short' => '0123456789']);
+ }
+ catch(Exception $e) {
+ $result = $e->getCode();
+ }
+ return $result;
+ },
+ 'assert' => function($result) {
+ return ($result == EQ_ERROR_INVALID_PARAM);
+ }
+ ],
+ '1021' => [
+ 'description' => "Checking usage validation 'amount/money' with invalid (not a number).",
+ 'act' => function () {
+ $result = 0;
+ try {
+ core\test\Test::create(['float_amount' => 'abc']);
+ }
+ catch(Exception $e) {
+ $result = $e->getCode();
+ }
+ return $result;
+ },
+ 'assert' => function($result) {
+ return ($result == EQ_ERROR_INVALID_PARAM);
+ }
+ ],
+ '1022' => [
+ 'description' => "Checking usage validation 'amount/money' with invalid (string).",
+ 'act' => function () {
+ $result = 0;
+ try {
+ core\test\Test::create(['float_amount' => '123,456']);
+ }
+ catch(Exception $e) {
+ $result = $e->getCode();
+ }
+ return $result;
+ },
+ 'assert' => function($result) {
+ return ($result == EQ_ERROR_INVALID_PARAM);
+ }
+ ],
+ '1023' => [
+ 'description' => "Checking usage validation 'amount/money' with invalid (decimal digits overflow).",
+ 'act' => function () {
+ $result = 0;
+ try {
+ core\test\Test::create(['float_amount' => 123.456789]);
+ }
+ catch(Exception $e) {
+ $result = $e->getCode();
+ }
+ return $result;
+ },
+ 'assert' => function($result) {
+ return ($result == EQ_ERROR_INVALID_PARAM);
+ }
+ ],
+ '1024' => [
+ 'description' => "Checking usage validation 'amount/money' with valid.",
+ 'act' => function () {
+ $result = 0;
+ try {
+ core\test\Test::create(['float_amount' => 123.4567]);
+ }
+ catch(Exception $e) {
+ $result = $e->getCode();
+ }
+ return $result;
+ },
+ 'assert' => function($result) {
+ return ($result == 0);
+ }
+ ],
+ '1031' => [
+ 'description' => "Checking usage validation 'currency' with invalid.",
+ 'act' => function () {
+ $result = 0;
+ try {
+ core\test\Test::create(['string_currency' => 'tralala']);
+ }
+ catch(Exception $e) {
+ $result = $e->getCode();
+ }
+ return $result;
+ },
+ 'assert' => function($result) {
+ return ($result == EQ_ERROR_INVALID_PARAM);
+ }
+ ],
+ '1032' => [
+ 'description' => "Checking usage validation 'currency' with valid.",
+ 'act' => function () {
+ $result = 0;
+ try {
+ core\test\Test::create(['string_currency' => 'USD']);
+ }
+ catch(Exception $e) {
+ $result = $e->getCode();
+ }
+ return $result;
+ },
+ 'assert' => function($result) {
+ return ($result == 0);
+ }
+ ],
+ '1041' => [
+ 'description' => "Checking usage validation 'datetime' with invalid (string).",
+ 'act' => function () {
+ $result = 0;
+ try {
+ core\test\Test::create(['datetime' => 'foo']);
+ }
+ catch(Exception $e) {
+ $result = $e->getCode();
+ }
+ return $result;
+ },
+ 'assert' => function($result) {
+ return ($result == EQ_ERROR_INVALID_PARAM);
+ }
+ ],
+ '1042' => [
+ 'description' => "Checking usage validation 'datetime' with valid.",
+ 'act' => function () {
+ $result = 0;
+ try {
+ core\test\Test::create(['datetime' => 1711990542]);
+ }
+ catch(Exception $e) {
+ $result = $e->getCode();
+ }
+ return $result;
+ },
+ 'assert' => function($result) {
+ return ($result == 0);
+ }
+ ]
+
+
+
+
+];
\ No newline at end of file
diff --git a/packages/core/uml/overview.or.equml b/packages/core/uml/overview.erd.json
similarity index 100%
rename from packages/core/uml/overview.or.equml
rename to packages/core/uml/overview.erd.json
diff --git a/packages/core/views/User.form.default.json b/packages/core/views/User.form.default.json
index e64259ea0..64fab218c 100644
--- a/packages/core/views/User.form.default.json
+++ b/packages/core/views/User.form.default.json
@@ -1,6 +1,6 @@
{
"name": "User",
- "description": "Simple form for displaying User",
+ "description": "Basic form for displaying User.",
"layout": {
"groups": [
{
@@ -8,7 +8,7 @@
"sections": [
{
"label": "User details",
- "id": "user_details",
+ "id": "section.details",
"rows": [
{
"columns": [
@@ -17,6 +17,7 @@
"align": "left",
"items": [
{
+ "id": "item.user_id",
"type": "field",
"value": "id",
"width": "50%",
diff --git a/packages/core/views/User.list.default.json b/packages/core/views/User.list.default.json
index 74d74e503..a17eb30c9 100644
--- a/packages/core/views/User.list.default.json
+++ b/packages/core/views/User.list.default.json
@@ -46,16 +46,6 @@
"readonly": true
}
},
- {
- "type": "field",
- "value": "firstname",
- "width": "20%"
- },
- {
- "type": "field",
- "value": "lastname",
- "width": "20%"
- },
{
"type": "field",
"value": "language",
diff --git a/packages/core/views/menu.settings.left.json b/packages/core/views/menu.settings.left.json
index 83f6bbdec..d334e3003 100644
--- a/packages/core/views/menu.settings.left.json
+++ b/packages/core/views/menu.settings.left.json
@@ -3,6 +3,7 @@
"access": {
"groups": ["setting.default.user"]
},
+ "search": true,
"layout": {
"items": [
{
diff --git a/public/console.php b/public/console.php
index 134879f48..bec2a3d33 100644
--- a/public/console.php
+++ b/public/console.php
@@ -4,415 +4,683 @@
Some Rights Reserved, Cedric Francoys, 2010-2023
Licensed under GNU LGPL 3 license
*/
-
-define('LOG_FILE_NAME', 'eq_error.log');
-$data = '';
+error_reporting(0);
// get log file, using variation from URL, if any
-$log_file = LOG_FILE_NAME.( (isset($_GET['f']) && strlen($_GET['f']))?('.'.$_GET['f']):'');
-
+$log_file = (isset($_GET['f']) && strlen($_GET['f']))?$_GET['f']:'eq_error.log';
// retrieve logs history (variations on filename)
$log_variations = [];
-foreach(glob('../log/'.LOG_FILE_NAME.'.*') as $file) {
+foreach(glob('../log/*.log') as $file) {
$log_variations[] = pathinfo($file, PATHINFO_EXTENSION);
}
-// get query from URL, if any
-$query = (isset($_GET['q']))?$_GET['q']:'';
-// adapt params
-if(isset($_GET['level']) && $_GET['level'] == '') {
- unset($_GET['level']);
-}
-if(isset($_GET['mode']) && $_GET['mode'] == '') {
- unset($_GET['mode']);
-}
-if(isset($_GET['date']) && $_GET['date'] == '') {
- unset($_GET['date']);
-}
+// no param given : frond-end App provider
+if(!count($_GET)) {
+ echo '
+
+
+
+
+
+
+
-
-
-
-
-Copied to clipboard
-