diff --git a/.gitignore b/.gitignore index f06cb89..601fc49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /vendor/* /vendor/ .phpunit.result.cache +.phpunit.cache .phpunit auth.json composer.lock diff --git a/README.md b/README.md index 39af52c..6324d10 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This README would normally document whatever steps are necessary to get your app ### How do I get set up? ### -* Summary of set up +* Summary of setup * Configuration * Dependencies * Database configuration diff --git a/backend/class/app.php b/backend/class/app.php index 79ad7af..cc9645f 100644 --- a/backend/class/app.php +++ b/backend/class/app.php @@ -1,155 +1,153 @@ requireResource('js', '/assets/requirejs/require.js', 0); - app::getResponse()->requireResource('js', '/assets/require.config.js', 1); - // requirecss must be issued earlier, if used - // app::getResponse()->requireResource('js', '/assets/require-css/css.js', 2); - // app::getResponse()->requireResource('script', - // "require(['require-css']);", 1 - // ); - /* app::getResponse()->requireResource('script', - "requirejs.config({ - baseUrl: 'library', - });", 0 - );*/ - self::$requireJsAdded = true; - } + if ($type === 'requirejs') { + // add requireJS if not already added + if (!self::$requireJsAdded) { + $response->requireResource('js', '/assets/requirejs/require.js', 0); + $response->requireResource('js', '/assets/require.config.js?t=' . time(), 1); + self::$requireJsAdded = true; + } - $assets = []; - if(is_array($asset)) { - $assets = $asset; - } else { - $assets = [$asset]; - } + if (is_array($asset)) { + $assets = $asset; + } else { + $assets = [$asset]; + } - foreach($assets as $a) { - if(!in_array($a, self::$requireJsAssets)) { - app::getResponse()->requireResource('script', "require(['{$a}'])"); - self::$requireJsAssets[] = $a; + foreach ($assets as $a) { + if (!in_array($a, self::$requireJsAssets)) { + $response->requireResource('script', "require(['$a'])"); + self::$requireJsAssets[] = $a; + } + } + } elseif ($type === 'requirecss') { + $type = 'css'; + if (is_array($asset)) { + foreach ($asset as $a) { + $response->requireResource($type, $a); + } + } else { + $response->requireResource($type, $asset); + } + } elseif (is_array($asset)) { + // TODO: require each resource? + foreach ($asset as $a) { + $response->requireResource($type, $a); + } + } else { + $response->requireResource($type, $asset); } - } + } - } else if($type === 'requirecss') { - $type = 'css'; - if(is_array($asset)) { - foreach($asset as $a) { - app::getResponse()->requireResource($type, $a); - } - } else { - app::getResponse()->requireResource($type, $asset); - } - } else { - // TODO: require each resource? - if(is_array($asset)) { - foreach($asset as $a) { - app::getResponse()->requireResource($type, $a); + /** + * [getUrlGenerator description] + * @return urlGeneratorInterface [description] + */ + public static function getUrlGenerator(): urlGeneratorInterface + { + if (self::$urlGenerator == null) { + self::$urlGenerator = new urlGenerator(); } - } else { - app::getResponse()->requireResource($type, $asset); - } + return self::$urlGenerator; } - } - - /** - * [protected description] - * @var \codename\core\generator\urlGeneratorInterface - */ - protected static $urlGenerator = null; - /** - * [getUrlGenerator description] - * @return \codename\core\generator\urlGeneratorInterface [description] - */ - public static function getUrlGenerator() : \codename\core\generator\urlGeneratorInterface { - if(self::$urlGenerator == null) { - self::$urlGenerator = new \codename\core\generator\urlGenerator(); + /** + * [setUrlGenerator description] + * @param urlGeneratorInterface $generator [description] + */ + public static function setUrlGenerator(urlGeneratorInterface $generator): void + { + self::$urlGenerator = $generator; } - return self::$urlGenerator; - } - /** - * [setUrlGenerator description] - * @param \codename\core\generator\urlGeneratorInterface $generator [description] - */ - public static function setUrlGenerator(\codename\core\generator\urlGeneratorInterface $generator) { - self::$urlGenerator = $generator; - } - - /** - * returns the current server/FE endpoint - * including Protocol and Port (on need) - * - * @return string [description] - */ - public static function getCurrentServerEndpoint() : string { - // url base prefix preparation - // - // vendors and services like AWS (especially ELBs) - // also provide a non-standard header like X-Forwarded-Port - // which is the same as with the protocol, but for ports - // - // NOTE: we handle X-Forwarded-Proto separately during request object creation (request\http) - // - $port = $_SERVER['HTTP_X_FORWARDED_PORT'] ?? $_SERVER['SERVER_PORT'] ?? null; - $proto = (($_SERVER['HTTPS'] ?? null) === 'on') ? 'https' : 'http'; - $portSuffix = (($proto === 'https' && $port != 443) || ($proto === 'http' && $port != 80)) ? (':'.$port) : ''; - $urlBase = $proto.'://'.$_SERVER['SERVER_NAME'].$portSuffix; - return $urlBase; - } + /** + * returns the current server/FE endpoint + * including Protocol and Port (on a need) + * + * @return string [description] + */ + public static function getCurrentServerEndpoint(): string + { + // url base prefix preparation + // + // vendors and services like AWS (especially ELBs) + // also provide a non-standard header like X-Forwarded-Port + // which is the same as with the protocol, but for ports + // + // NOTE: we handle X-Forwarded-Proto separately during request object creation (request\http) + // + $port = $_SERVER['HTTP_X_FORWARDED_PORT'] ?? $_SERVER['SERVER_PORT'] ?? null; + $proto = (($_SERVER['HTTPS'] ?? null) === 'on') ? 'https' : 'http'; + $portSuffix = (($proto === 'https' && $port != 443) || ($proto === 'http' && $port != 80)) ? (':' . $port) : ''; + return $proto . '://' . $_SERVER['SERVER_NAME'] . $portSuffix; + } + /** + * {@inheritDoc} + * this class may not be run + */ + public function run(): void + { + throw new exception(self::EXCEPTION_CORE_UI_APP_ILLEGAL_CALL, exception::$ERRORLEVEL_FATAL); + } } diff --git a/backend/class/context/crud.php b/backend/class/context/crud.php index 9aaa3f5..0d5f09c 100644 --- a/backend/class/context/crud.php +++ b/backend/class/context/crud.php @@ -1,195 +1,329 @@ It seems that I did not receive it in the current request container. + * It seems that I did not receive it in the current request container. * @var string */ - CONST EXCEPTION_VIEW_CRUD_EDIT_PRIMARYKEYNOTSENT = 'EXCEPTION_VIEW_CRUD_EDIT_PRIMARYKEYNOTSENT'; + public const string EXCEPTION_VIEW_CRUD_EDIT_PRIMARYKEYNOTSENT = 'EXCEPTION_VIEW_CRUD_EDIT_PRIMARYKEYNOTSENT'; /** * You loaded a view that requires the model's primary key to be sent. - *
It seems that I did not receive it in the current request container. + * It seems that I did not receive it in the current request container. * @var string */ - CONST EXCEPTION_VIEW_CRUD_DELETE_PRIMARYKEYNOTSENT = 'EXCEPTION_VIEW_CRUD_DELETE_PRIMARYKEYNOTSENT'; + public const string EXCEPTION_VIEW_CRUD_DELETE_PRIMARYKEYNOTSENT = 'EXCEPTION_VIEW_CRUD_DELETE_PRIMARYKEYNOTSENT'; /** * You are trying to use the nested instance of a CRUD editor. - *
Unfortunately it seems to remain NULL until this point of time. + * Unfortunately, it seems to remain NULL until this point of time. * @var string */ - CONST EXCEPTION_GETCRUDINSTANCE_CRUDPROPERTYISNULL = 'EXCEPTION_GETCRUDINSTANCE_CRUDPROPERTYISNULL'; + public const string EXCEPTION_GETCRUDINSTANCE_CRUDPROPERTYISNULL = 'EXCEPTION_GETCRUDINSTANCE_CRUDPROPERTYISNULL'; /** * Overwrite what model to use in the CRUD generator - * @var string + * @var null|string */ - protected $modelName = null; + protected ?string $modelName = null; /** * Overwrite the name of the app the requested model is located - * @var string + * @var null|string */ - protected $modelApp = null; + protected ?string $modelApp = null; /** * Holds the model for this CRUD generator - * @var \codename\core\model + * @var null|model */ - protected $model = null; + protected ?model $model = null; /** * Holds the CRUD instance for this request - * @var \codename\core\ui\crud + * @var null|\codename\core\ui\crud */ - protected $crud = null; + protected ?\codename\core\ui\crud $crud = null; /** * Creates the CRUD instance in the context instance - *
Sends the name of the primary key tot he response - * @return \codename\core\ui\context\crud + * Sends the name of the primary key to the response + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception * @todo Why do we have to set the template here again? */ - public function __construct() { - $this->getResponse()->setData('primarykey', $this->getModelinstance()->getPrimarykey()); + public function __construct() + { + $this->getResponse()->setData('primarykey', $this->getModelinstance()->getPrimaryKey()); // $this->getResponse()->setData('template', 'basic'); $dict = [ 'context' => $this->getRequest()->getData('context'), - 'view' => $this->getRequest()->getData('context').'_'.$this->getRequest()->getData('view') + 'view' => $this->getRequest()->getData('context') . '_' . $this->getRequest()->getData('view'), ]; - if($this->getRequest()->getData('action')) { - $dict['action'] = $this->getRequest()->getData('context').'_'.$this->getRequest()->getData('view').'___'.$this->getRequest()->getData('action'); + if ($this->getRequest()->getData('action')) { + $dict['action'] = $this->getRequest()->getData('context') . '_' . $this->getRequest()->getData('view') . '___' . $this->getRequest()->getData('action'); } - foreach($dict as &$d) { - if($d !== null) { - $d = app::getTranslate()->translate('CRUD.'.$d); - } + foreach ($dict as &$d) { + if ($d !== null) { + $d = app::getTranslate()->translate('CRUD.' . $d); + } } $this->getResponse()->setData('crud_label', $dict); - $this->setCrudinstance(new \codename\core\ui\crud($this->getModelinstance())); + $this->setCrudInstance(new \codename\core\ui\crud($this->getModelinstance())); // hook into crud instance init - // we need to change the output type to bare json config - if($this->getResponse() instanceof \codename\rest\response\json) { - $this->getCrudinstance()->outputFormConfig = true; + // we need to change the output type to bare JSON config + if ($this->getResponse() instanceof json) { + $this->getCrudInstance()->outputFormConfig = true; } return $this; } + /** + * Returns the exact model instance that was requested + * @return model + * @throws ReflectionException + * @throws exception + * @access public + */ + public function getModelinstance(): model + { + if (is_null($this->model)) { + $this->setModelinstance($this->getModel($this->getModelname(), $this->getModelapp())); + } + return $this->model; + } + + /** + * Stores the model instance in this class + * @param model $model + * @return void + * @access public + */ + public function setModelinstance(model $model): void + { + $this->model = $model; + } + + /** + * Returns the name of the requested model + * @return string + * @access public + */ + public function getModelname(): string + { + if (is_null($this->modelName)) { + return $this->getRequest()->getData('context'); + } + return $this->modelName; + } + + /** + * Sets the name of the model that will be requested + * @param string $modelName + * @return void + * @access public + */ + public function setModelname(string $modelName): void + { + $this->modelName = $modelName; + } + + /** + * Returns the app the requested model is located in + * @return string + * @throws ReflectionException + * @throws exception + * @access public + */ + public function getModelapp(): string + { + if (is_null($this->modelApp)) { + return app::getApp(); + } + return $this->modelApp; + } + + /** + * Sets the app the requested model is located in + * @param string $modelApp + * @return void + * @access public + */ + public function setModelapp(string $modelApp): void + { + $this->modelApp = $modelApp; + } + + /** + * Set the CRUD instance of this context + * @param \codename\core\ui\crud $crud + * @return void + */ + protected function setCrudInstance(\codename\core\ui\crud $crud): void + { + $this->crud = $crud; + } + + /** + * Return the CRUD instance of this context + * @return \codename\core\ui\crud + * @throws exception + */ + public function getCrudInstance(): \codename\core\ui\crud + { + if (is_null($this->crud)) { + throw new exception(self::EXCEPTION_GETCRUDINSTANCE_CRUDPROPERTYISNULL, exception::$ERRORLEVEL_WARNING, ($this->model->getPrimaryKey() ?? null)); + } + return $this->crud; + } + /** * Using the CRUD generator to generate the CRUD config * @return void + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception * @access public */ - public function view_crud_config () { - $this->getResponse()->setData('context', 'crud'); - $this->getCrudinstance()->listconfig(); - return; + public function view_crud_config(): void + { + $this->getResponse()->setData('context', 'crud'); + $this->getCrudInstance()->listconfig(); } /** * Crud Stats * also to be used for async counts * @return void + * @throws ReflectionException + * @throws exception */ - public function view_crud_stats () { - $this->getResponse()->setData('context', 'crud'); - $this->getCrudinstance()->stats(); - return; + public function view_crud_stats(): void + { + $this->getResponse()->setData('context', 'crud'); + $this->getCrudInstance()->stats(); } /** - * If there are overridden crud_list functions + * If there are overridden crud_list functions, * this may be used for doing this special override * @return void */ - public function action_crud_stats() { - return; + public function action_crud_stats(): void + { } /** * Using the CRUD generator to generate the list page * @return void + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + * @throws noticeException * @access public */ - public function view_crud_list () { + public function view_crud_list(): void + { $this->getResponse()->setData('context', 'crud'); - if($this->getRequest()->getData('action') == 'crud_stats') { - $this->getCrudinstance()->stats(); + if ($this->getRequest()->getData('action') == 'crud_stats') { + $this->getCrudInstance()->stats(); } else { - $this->getCrudinstance()->listview(); + $this->getCrudInstance()->listview(); } - return; } /** * Using the CRUD generator to generate the edit page * @return void + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception * @access public */ - public function view_crud_edit () { + public function view_crud_edit(): void + { $this->getResponse()->setData('context', 'crud'); - $primaryKey = $this->getModelinstance()->getPrimarykey(); + $primaryKey = $this->getModelinstance()->getPrimaryKey(); - if(!$this->getRequest()->isDefined($primaryKey) || strlen($this->getRequest()->getData($primaryKey)) == 0) { - throw new \codename\core\exception(self::EXCEPTION_VIEW_CRUD_EDIT_PRIMARYKEYNOTSENT, \codename\core\exception::$ERRORLEVEL_WARNING, $primaryKey); + if (!$this->getRequest()->isDefined($primaryKey) || strlen($this->getRequest()->getData($primaryKey)) == 0) { + throw new exception(self::EXCEPTION_VIEW_CRUD_EDIT_PRIMARYKEYNOTSENT, exception::$ERRORLEVEL_WARNING, $primaryKey); } - $this->getCrudinstance()->edit($this->getRequest()->getData($primaryKey)); - return; + $this->getCrudInstance()->edit($this->getRequest()->getData($primaryKey)); } /** * Using the CRUD generator to generate the show page - * @author Kevin Dargel * @return void + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception * @access public */ - public function view_crud_show () { + public function view_crud_show(): void + { $this->getResponse()->setData('context', 'crud'); $this->getResponse()->setData('view', 'crud_show'); - $primaryKey = $this->getModelinstance()->getPrimarykey(); + $primaryKey = $this->getModelinstance()->getPrimaryKey(); - if(!$this->getRequest()->isDefined($primaryKey) || strlen($this->getRequest()->getData($primaryKey)) == 0) { - throw new \codename\core\exception(self::EXCEPTION_VIEW_CRUD_EDIT_PRIMARYKEYNOTSENT, \codename\core\exception::$ERRORLEVEL_WARNING, $primaryKey); + if (!$this->getRequest()->isDefined($primaryKey) || strlen($this->getRequest()->getData($primaryKey)) == 0) { + throw new exception(self::EXCEPTION_VIEW_CRUD_EDIT_PRIMARYKEYNOTSENT, exception::$ERRORLEVEL_WARNING, $primaryKey); } - $this->getCrudinstance()->show($this->getRequest()->getData($primaryKey)); - return; + $this->getCrudInstance()->show($this->getRequest()->getData($primaryKey)); } /** - * Using the CRUD generator to generate the delete page + * Using the CRUD generator to generate the deleted page * @return void + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception * @access public */ - public function view_crud_delete () { + public function view_crud_delete(): void + { $this->getResponse()->setData('context', 'crud'); - $primaryKey = $this->getModelinstance()->getPrimarykey(); + $primaryKey = $this->getModelinstance()->getPrimaryKey(); - if(!$this->getRequest()->isDefined($primaryKey) || strlen($this->getRequest()->getData($primaryKey)) == 0) { - throw new \codename\core\exception(self::EXCEPTION_VIEW_CRUD_DELETE_PRIMARYKEYNOTSENT, \codename\core\exception::$ERRORLEVEL_WARNING, $primaryKey); + if (!$this->getRequest()->isDefined($primaryKey) || strlen($this->getRequest()->getData($primaryKey)) == 0) { + throw new exception(self::EXCEPTION_VIEW_CRUD_DELETE_PRIMARYKEYNOTSENT, exception::$ERRORLEVEL_WARNING, $primaryKey); } // If confirmed, delete action - if($this->getRequest()->isDefined('__confirm')) { + if ($this->getRequest()->isDefined('__confirm')) { $this->getModelinstance()->delete($this->getRequest()->getData($primaryKey)); $this->getResponse()->setRedirect('', $this->getRequest()->getData('context'), 'crud_list'); $this->getResponse()->doRedirect(); @@ -199,251 +333,167 @@ public function view_crud_delete () { $this->getResponse()->setData('keyname', $primaryKey); $this->getResponse()->setData('keyvalue', $this->getRequest()->getData($primaryKey)); $this->getResponse()->setData('modelObject', $this->getModelinstance()->load($this->getRequest()->getData($primaryKey))); - return; } /** - * Using the CRUD generator to generate the create page + * Using the CRUD generator to generate the creation page * @return void + * @throws ReflectionException + * @throws DateMalformedStringException + * @throws exception */ - public function view_crud_create() { + public function view_crud_create(): void + { $this->getResponse()->setData('context', 'crud'); - $this->getCrudinstance()->create(); - return; + $this->getCrudInstance()->create(); } /** * Using the CRUD generator to overwrite/edit multiple datasets * @return void + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception */ - public function view_bulk_edit() { + public function view_bulk_edit(): void + { $this->getResponse()->setData('context', 'crud'); - $this->getCrudinstance()->bulkEdit(); - return; + $this->getCrudInstance()->bulkEdit(); } /** * Using the CRUD generator to delete multiple datasets at once * @return void + * @throws exception */ - public function view_bulk_delete() { + public function view_bulk_delete(): void + { $this->getResponse()->setData('context', 'crud'); - $this->getCrudinstance()->bulkDelete(); - return; + $this->getCrudInstance()->bulkDelete(); } /** * [action_import description] - * @return [type] [description] + * @return void [type] [description] + * @throws ReflectionException + * @throws exception */ - public function action_import() { - $this->getResponse()->setData('context', 'crud'); - if($this->getCrudinstance()->getConfig()->exists('import>_security>group')) { - $group = $this->getCrudinstance()->getConfig()->get('import>_security>group'); - if(\codename\core\app::getAuth()->memberOf($group)) { - // get import file - $request = $this->getRequest(); - if($request instanceof \codename\core\request\filesInterface) { - $importFileUpload = $request->getFiles()['crud_import_file'] ?? null; - if($importFileUpload && $importFileUpload['tmp_name']) { - $json = json_decode(file_get_contents($importFileUpload['tmp_name']), true); - $this->getCrudinstance()->import($json); + public function action_import(): void + { + $this->getResponse()->setData('context', 'crud'); + if ($this->getCrudInstance()->getConfig()->exists('import>_security>group')) { + $group = $this->getCrudInstance()->getConfig()->get('import>_security>group'); + if (app::getAuth()->memberOf($group)) { + // get an import file + $request = $this->getRequest(); + if ($request instanceof filesInterface) { + $importFileUpload = $request->getFiles()['crud_import_file'] ?? null; + if ($importFileUpload && $importFileUpload['tmp_name']) { + $json = json_decode(file_get_contents($importFileUpload['tmp_name']), true); + $this->getCrudInstance()->import($json); + } else { + throw new exception('CRUD_IMPORT_INVALID_IMPORT_FILE_UPLOAD', exception::$ERRORLEVEL_ERROR); + } + } else { + throw new exception('CRUD_IMPORT_INVALID_REQUEST', exception::$ERRORLEVEL_ERROR); + } } else { - throw new exception('CRUD_IMPORT_INVALID_IMPORT_FILE_UPLOAD', exception::$ERRORLEVEL_ERROR); + throw new exception('CRUD_IMPORT_NOT_ALLOWED_BY_AUTH', exception::$ERRORLEVEL_ERROR); } - } else { - throw new exception('CRUD_IMPORT_INVALID_REQUEST', exception::$ERRORLEVEL_ERROR); - } } else { - throw new exception('CRUD_IMPORT_NOT_ALLOWED_BY_AUTH', exception::$ERRORLEVEL_ERROR); + throw new exception('CRUD_IMPORT_NOT_ALLOWED_BY_CONFIG', exception::$ERRORLEVEL_ERROR); } - } else { - throw new exception('CRUD_IMPORT_NOT_ALLOWED_BY_CONFIG', exception::$ERRORLEVEL_ERROR); - } } /** * Enables export of the current crud_list + * @return void + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + * @throws noticeException */ - public function action_export() { - $this->getResponse()->setData('context', 'crud'); - - if($this->getCrudinstance()->getConfig()->exists('export>_security>group')) { - $group = $this->getCrudinstance()->getConfig()->get('export>_security>group'); - if(\codename\core\app::getAuth()->memberOf($group)) { - - // handle raw mode - $rawMode = false; - if($this->getRequest()->getData('export_mode') === 'raw') { - if(!$this->getCrudinstance()->getConfig()->get('export>allowRaw')) { - throw new exception('EXPORT_MODE_RAW_NOT_ALLOWED', exception::$ERRORLEVEL_ERROR); - } - - // perform raw modifications - $rawMode = true; - } - - // selected export ids - if($selectedIds = $this->getRequest()->getData('export_selected_id')) { - $this->getCrudinstance()->getMyModel()->addDefaultFilter( - $this->getCrudinstance()->getMyModel()->getPrimaryKey(), - $selectedIds - ); - } - - $this->getCrudinstance()->export($rawMode); + public function action_export(): void + { + $this->getResponse()->setData('context', 'crud'); - // should be customizable: (e.g. excel, standard, ...) exports. - $exportType = $this->getRequest()->getData('export_type'); + if ($this->getCrudInstance()->getConfig()->exists('export>_security>group')) { + $group = $this->getCrudInstance()->getConfig()->get('export>_security>group'); + if (app::getAuth()->memberOf($group)) { + // handle raw mode + $rawMode = false; + if ($this->getRequest()->getData('export_mode') === 'raw') { + if (!$this->getCrudInstance()->getConfig()->get('export>allowRaw')) { + throw new exception('EXPORT_MODE_RAW_NOT_ALLOWED', exception::$ERRORLEVEL_ERROR); + } + + // perform raw modifications + $rawMode = true; + } - // check export types - if(!in_array($exportType, $this->getCrudinstance()->getConfig()->get('export>allowedTypes'))) { - throw new exception('EXPORT_TYPE_NOT_ALLOWED', exception::$ERRORLEVEL_ERROR, $exportType); - } + // selected export ids + if ($selectedIds = $this->getRequest()->getData('export_selected_id')) { + $this->getCrudInstance()->getMyModel()->addDefaultFilter( + $this->getCrudInstance()->getMyModel()->getPrimaryKey(), + $selectedIds + ); + } - $exportClass = \codename\core\app::getInheritedClass('export_'.$exportType); - $export = new $exportClass(); + $this->getCrudInstance()->export($rawMode); - if($export instanceof \codename\core\export\exportInterface) { + // should be customizable: (e.g., excel, standard, ...) exports. + $exportType = $this->getRequest()->getData('export_type'); - if(!$rawMode) { - foreach($this->getResponse()->getData('visibleFields') as $field) { - $export->addField(new \codename\core\value\text($field)); + // check export types + if (!in_array($exportType, $this->getCrudInstance()->getConfig()->get('export>allowedTypes'))) { + throw new exception('EXPORT_TYPE_NOT_ALLOWED', exception::$ERRORLEVEL_ERROR, $exportType); } - } else { - if($this->getResponse()->getData('rows')[0] ?? false) { - foreach($this->getResponse()->getData('rows')[0] as $key => $irrelevantValue) { - $export->addField(new \codename\core\value\text($key)); - } + + $exportClass = app::getInheritedClass('export_' . $exportType); + $export = new $exportClass(); + + if ($export instanceof exportInterface) { + if (!$rawMode) { + foreach ($this->getResponse()->getData('visibleFields') as $field) { + $export->addField(new text($field)); + } + } elseif ($this->getResponse()->getData('rows')[0] ?? false) { + foreach ($this->getResponse()->getData('rows')[0] as $key => $irrelevantValue) { + $export->addField(new text($key)); + } + } else { + // error? + } + + foreach ($this->getResponse()->getData('rows') as $row) { + $export->addRow(new datacontainer($row)); + } + + // foolish. + $fileExtension = match ($exportType) { + 'json' => 'json', + 'csv_excel' => 'csv', + default => throw new LogicException("Method not implemented."), + }; + + $filename = 'Export_' . $this->getCrudInstance()->getMyModel()->getIdentifier() . '_' . time() . '.' . $fileExtension; + $exportedFile = '/tmp/' . $filename; + + $export->setFilename($exportedFile)->export(); + + file::downloadToClient($exportedFile, $filename); + + // + // TODO: delete tmp file - this requires downloadToClient NOT to stop things executing. + // } else { - // error? + throw new exception('EXPORT_CLASS_INVALID', exception::$ERRORLEVEL_FATAL, $exportClass); } - } - - foreach($this->getResponse()->getData('rows') as $row) { - $export->addRow(new \codename\core\datacontainer($row)); - } - - // stupid. - switch($exportType) { - case 'json': - $fileExtension = 'json'; - break; - case 'csv_excel': - $fileExtension = 'csv'; - break; - } - - $filename = 'Export_' . $this->getCrudinstance()->getMyModel()->getIdentifier() . '_' . time() . '.' . $fileExtension; - $exportedFile = '/tmp/' . $filename; - - $export->setFilename($exportedFile)->export(); - - \codename\core\helper\file::downloadToClient($exportedFile, $filename); - - // - // TODO: delete tmp file - this requires downloadToClient NOT to stop things executing. - // } else { - throw new exception('EXPORT_CLASS_INVALID', exception::$ERRORLEVEL_FATAL, $exportClass); + throw new exception('EXPORT_DISALLOWED_BY_AUTH', exception::$ERRORLEVEL_ERROR); } } else { - throw new exception('EXPORT_DISALLOWED_BY_AUTH', exception::$ERRORLEVEL_ERROR); - } - } else { - throw new exception('EXPORT_DISALLOWED_BY_CONFIG', exception::$ERRORLEVEL_ERROR); - } - return; - } - - /** - * Returns the name of the requested model - * @return string - * @access public - */ - public function getModelname() : string { - if(is_null($this->modelName)) { - return $this->getRequest()->getData('context'); + throw new exception('EXPORT_DISALLOWED_BY_CONFIG', exception::$ERRORLEVEL_ERROR); } - return $this->modelName; } - - /** - * Returns the app the requested model is located in - * @return string - * @access public - */ - public function getModelapp() : string { - if(is_null($this->modelApp)) { - return \codename\core\app::getApp(); - } - return $this->modelApp; - } - - /** - * Sets the app the requested model is located in - * @param string $modelApp - * @return void - * @access public - */ - public function setModelapp(string $modelApp) { - $this->modelApp = $modelApp; - return; - } - - /** - * Sets the name of the model that will be requested - * @param string $modelName - * @return void - * @access public - */ - public function setModelname(string $modelName) { - $this->modelName = $modelName; - return; - } - - /** - * Stores the model instance in this class - * @param \codename\core\model $model - * @return void - * @access public - */ - public function setModelinstance(\codename\core\model $model) { - $this->model = $model; - return; - } - - /** - * Returns the exact model instance that was requested - * @return \codename\core\model - * @access public - */ - public function getModelinstance() : \codename\core\model { - if(is_null($this->model)) { - $this->setModelinstance($this->getModel($this->getModelname(), $this->getModelapp())); - } - return $this->model; - } - - /** - * Set the CRUD instance of this context - * @param \codename\core\ui\crud $crud - * @return void - */ - protected function setCrudinstance(\codename\core\ui\crud $crud) { - $this->crud = $crud; - return; - } - - /** - * Return the CRUD instance of this context - * @return \codename\core\ui\crud - */ - public function getCrudinstance() : \codename\core\ui\crud { - if(is_null($this->crud)) { - throw new \codename\core\exception(self::EXCEPTION_GETCRUDINSTANCE_CRUDPROPERTYISNULL, \codename\core\exception::$ERRORLEVEL_WARNING, ($this->model->getPrimarykey() ?? null)); - } - return $this->crud; - } - } diff --git a/backend/class/crud.php b/backend/class/crud.php index 8c722b8..eb23da7 100644 --- a/backend/class/crud.php +++ b/backend/class/crud.php @@ -1,140 +1,283 @@ It is capable of Creating, Reading, Updating and Deleting data in the models. - * It utilizes several frontend resources located in the core frontend folder. - *
Override these templates by adding these files in your application's directory + * It is capable of Creating, Reading, Updating and Deleting data in the models. + * It uses several frontend resources located in the core frontend folder. + * Override these templates by adding these files in your application's directory * @package codename\core\ui */ -class crud extends \codename\core\bootstrapInstance { - +class crud extends bootstrapInstance +{ /** * The desired field cannot be found in the current model. * @var string */ - CONST EXCEPTION_MAKEFORM_FIELDNOTFOUNDINMODEL = 'EXCEPTION_MAKEFORM_FIELDNOTFOUNDINMODEL'; + public const string EXCEPTION_MAKEFORM_FIELDNOTFOUNDINMODEL = 'EXCEPTION_MAKEFORM_FIELDNOTFOUNDINMODEL'; /** * The desired field cannot be found in the current model. * @var string */ - CONST EXCEPTION_MAKEFIELD_FIELDNOTFOUNDINMODEL = 'EXCEPTION_MAKEFIELD_FIELDNOTFOUNDINMODEL'; + public const string EXCEPTION_MAKEFIELD_FIELDNOTFOUNDINMODEL = 'EXCEPTION_MAKEFIELD_FIELDNOTFOUNDINMODEL'; /** * The foreign-model reference object is not valid. * @todo use value-object here * @var string */ - CONST EXCEPTION_MAKEFIELD_INVALIDREFERENCEOBJECT = 'EXCEPTION_MAKEFIELD_INVALIDREFERENCEOBJECT'; + public const string EXCEPTION_MAKEFIELD_INVALIDREFERENCEOBJECT = 'EXCEPTION_MAKEFIELD_INVALIDREFERENCEOBJECT'; /** * The model order object is not valid. * @todo use value-object here * @var string */ - CONST EXCEPTION_MAKEFIELD_INVALIDORDEROBJECT = 'EXCEPTION_MAKEFIELD_INVALIDORDEROBJECT'; + public const string EXCEPTION_MAKEFIELD_INVALIDORDEROBJECT = 'EXCEPTION_MAKEFIELD_INVALIDORDEROBJECT'; /** * The model filter object is not valid. * @todo use value-object here * @var string */ - CONST EXCEPTION_MAKEFIELD_INVALIDFILTEROBJECT = 'EXCEPTION_MAKEFIELD_INVALIDFILTEROBJECT'; + public const string EXCEPTION_MAKEFIELD_INVALIDFILTEROBJECT = 'EXCEPTION_MAKEFIELD_INVALIDFILTEROBJECT'; /** - * The model filter flag operator is not valid. should be = or != + * The model filter flag operator is not valid. Should be = or != * @todo use value-object here * @var string */ - CONST EXCEPTION_MAKEFIELD_FILTER_FLAG_INVALIDOPERATOR = 'EXCEPTION_MAKEFIELD_FILTER_FLAG_INVALIDOPERATOR'; + public const string EXCEPTION_MAKEFIELD_FILTER_FLAG_INVALIDOPERATOR = 'EXCEPTION_MAKEFIELD_FILTER_FLAG_INVALIDOPERATOR'; /** * The action button object is not valid * @todo use value-object here * @var string */ - CONST EXCEPTION_ADDACTION_INVALIDACTIONOBJECT = 'EXCEPTION_ADDACTION_INVALIDACTIONOBJECT'; + public const string EXCEPTION_ADDACTION_INVALIDACTIONOBJECT = 'EXCEPTION_ADDACTION_INVALIDACTIONOBJECT'; /** * Contains the request entry that will hold all filters applied to the crud list * @var string */ - CONST CRUD_FILTER_IDENTIFIER = '_cf'; - + public const string CRUD_FILTER_IDENTIFIER = '_cf'; + /** + * exception thrown if a given model does not define child configuration + * but crud tries to use it + * @var string + */ + public const string EXCEPTION_CRUD_CHILDREN_CONFIG_MODEL_CONFIG_CHILDREN_IS_NULL = 'EXCEPTION_CRUD_CHILDREN_CONFIG_MODEL_CONFIG_CHILDREN_IS_NULL'; + /** + * If true, does not render the form + * Instead, output form config + * @var bool + */ + public bool $outputFormConfig = false; + /** + * This event will be fired whenever a method of this CRUD instance generates a form instance. + * Use this event to alter the current form of the CRUD instance (e.g., for asking for more fields) + * @var event + */ + public event $eventCrudFormInit; + /** + * This event is fired before the validation starts. + * @example Imagine cases where you don't want a user to input data, but you must + * add it to the entry because the missing fields would violate the model's + * constraints. Here you can do anything you want with the entry array. + * @var event + */ + public event $eventCrudBeforeValidation; + /** + * This event is fired after validation has been successful. + * @var event + */ + public event $eventCrudAfterValidation; + /** + * This event is fired after validation has been successful. + * We might run additional validators here. + * Output must be either null, empty array or errors found in additional validators + * @var event + */ + public event $eventCrudValidation; + /** + * This event is fired whenever the CRUD generator wants to save a validated entry (or updates) + * to a model. It is given the $data and must return the $data. + * @example Imagine you want to manipulate entries on a model when saving the entry + * from the CRUD generator. This version will happen after the validation. + * @var event + */ + public event $eventCrudBeforeSave; + /** + * This event is fired whenever the CRUD generator successfully completed an operation + * to a model. It is given the $data. + * @var event + */ + public event $eventCrudSuccess; + /** + * crud is in readonly mode + * @var bool + */ + public bool $readOnly = false; + /** + * Provides a way to hook into the formfield creation process + * the fielddata array used for the field-.ctor is being used as argument + * @var callable + */ + public $onCreateFormfield = null; + /** + * Provides a way to hook into when the formfield has been created + * the created field is being used as an argument + * @var callable + */ + public $onFormfieldCreated = null; /** * Contains the model this CRUD instance is based upon - * @var \codename\core\model + * @var model */ - protected $model = null; - + protected model $model; /** * Contains the form instance we are working with - * @var \codename\core\ui\form + * @var form */ - protected $form = null; - + protected form $form; /** * Contains all the fields that will be displayed in the CRUD generator * @var array */ - protected $fields = array(); - + protected array $fields = []; /** * Contains all fields Configurations that are displayed in the CRUD generator * @var array */ - protected $fieldsformConfig = array(); - + protected array $fieldsformConfig = []; /** * Contains the ID of the CRUD form * @var string */ - protected $form_id = 'crud_default_form'; - + protected string $form_id = 'crud_default_form'; /** * Contains the dataset of the model. May be empty when creating a new entry - * @var \codename\core\datacontainer + * @var null|datacontainer */ - protected $data = null; - + protected ?datacontainer $data = null; /** * contains an instance of config storage - * @var \codename\core\config + * @var config */ - protected $config = null; - + protected config $config; /** * Contains a list of fields and their modifiers * @var array $modifiers */ - protected $modifiers = array(); - + protected array $modifiers = []; /** * Contains a list of row modifiers (callables) * @var callable[] $rowModifiers */ - protected $rowModifiers = array(); - + protected array $rowModifiers = []; /** - * If true, does not render the form - * Instead, output form config + * [protected description] + * @var crud[] + */ + protected array $childCruds = []; + /** + * Cache configurations + * @var bool + */ + protected bool $useConfigCache = true; + /** + * [protected description] + * @var callable[] + */ + protected array $resultsetModifiers = []; + /** + * default column ordering + * @var string[] + */ + protected array $columnOrder = []; + /** + * [protected description] + * @var array|null + */ + protected ?array $resultData = null; + /** + * internal and temporary pagination switch (for exporting) + * @var bool + */ + protected bool $allowPagination = true; + /** + * defines raw, unformatted mode + * @var bool + */ + protected bool $rawMode = false; + /** + * [protected description] + * @var null|array + */ + protected ?array $formNormalizationData = null; + /** + * [protected description] + * @var null|string + */ + protected ?string $crudSeekOverridePkeyOrder = null; + /** + * list of fields that are configured + * to just provide a basic configuration + * and skip unnecessary stuff (e.g., FKEY value fetching) + * @var string[] + */ + protected array $customizedFields = []; + /** + * customized, provided filters + * @var array + */ + protected array $providedFilters = []; + /** + * whether the crud_list + * should provide raw result parts + * from the model query * @var bool */ - public $outputFormConfig = false; + protected bool $provideRawData = false; + /** + * [protected description] + * @var model[] + */ + protected array $cachedModels = []; /** - * Creates the instance and sets the $model of this instance. Also creates the form instance - * @param \codename\core\model $model - * @param array|null $requestData - * @param string $crudConfig [optional explicit crud config] + * Creates the instance and sets the $model of this instance. Also create the form instance + * @param model $model + * @param array|null $requestData + * @param string $crudConfig [optional explicit crud config] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception */ - public function __CONSTRUCT(\codename\core\model $model, ?array $requestData = null, string $crudConfig = '') { + public function __construct(model $model, ?array $requestData = null, string $crudConfig = '') + { $this->eventCrudFormInit = new event('EVENT_CRUD_FORM_INIT'); $this->eventCrudBeforeValidation = new event('EVENT_CRUD_BEFORE_VALIDATION'); $this->eventCrudAfterValidation = new event('EVENT_CRUD_AFTER_VALIDATION'); @@ -142,2843 +285,2594 @@ public function __CONSTRUCT(\codename\core\model $model, ?array $requestData = n $this->eventCrudBeforeSave = new event('EVENT_CRUD_BEFORE_SAVE'); $this->eventCrudSuccess = new event('EVENT_CRUD_SUCCESS'); $this->model = $model; - if($requestData != null) { - $this->setFormNormalizationData($requestData); + if ($requestData != null) { + $this->setFormNormalizationData($requestData); } $this->setConfig($crudConfig); $this->setChildCruds(); $this->updateChildCrudConfigs(); - $this->form = new \codename\core\ui\form(array( - 'form_action' => ui\app::getUrlGenerator()->generateFromParameters(array( + $this->form = new form([ + 'form_action' => ui\app::getUrlGenerator()->generateFromParameters([ 'context' => $this->getRequest()->getData('context'), - 'view' => $this->getRequest()->getData('view') - )), + 'view' => $this->getRequest()->getData('view'), + ]), 'form_method' => 'post', - 'form_id' => $this->form_id - )); + 'form_id' => $this->form_id, + ]); return $this; } /** - * [protected description] - * @var \codename\core\ui\crud[] + * reads config from the 'children' key + * and creates instances for those children (cruds) + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception */ - protected $childCruds = []; + protected function setChildCruds(): void + { + // apply nested children config + if ($this->config->exists('children')) { + foreach ($this->config->get('children') as $child) { + // + // the primary child configuration + // in the model + // + $childConfig = $this->model->config->get('children>' . $child); + + // + // optional crud/form overrides + // + $childCrudConfig = $this->config->get('children_config>' . $child); + + if ($childConfig != null) { + // we handle a single-ref foreign key field as base + // for a nested model as a virtual object key + if ($childConfig['type'] == 'foreign') { + // get the foreign key config + $foreignConfig = $this->model->config->get('foreign>' . $childConfig['field']); + // get the respective model + $childModel = $this->getModel($foreignConfig['model'], $foreignConfig['app'] ?? '', $foreignConfig['vendor'] ?? ''); + // build child crud + $crud = new crud($childModel, $this->getFormNormalizationData()[$child] ?? []); + + // + // Handle optional configs + // + if (isset($childCrudConfig['crud'])) { + $crud->setConfig($childCrudConfig['crud']); + } + if (isset($childCrudConfig['form'])) { + $crud->useForm($childCrudConfig['form']); + } + + // make only a part of the request visible to the crud instance + $crud->setFormNormalizationData($this->getFormNormalizationData()[$child] ?? []); + + // store it for later + $this->childCruds[$child] = $crud; + + // + // CHANGED/FEATURE force_virtual_join 2020-07-21 + // This method uses the new feature of the core framework + // this allows virtualizing the table join + // to avoid RDBMS join limitations by abstracting the whole thing. + // + // You have to enable 'force_virtual_join' in the respective model child config + // Cruds enable this feature automatically, while you may opt in into its usage + // when querying models regularly. + // + if ($childConfig['force_virtual_join'] ?? false) { + $virtualJoinModel = $crud->getMyModel(); + $virtualJoinModel->setForceVirtualJoin(true); + $this->getMyModel()->addModel($virtualJoinModel, join::TYPE_LEFT, $childConfig['field'], $foreignConfig['key']); + } else { + // join the model upon the current + $this->getMyModel()->addModel($crud->getMyModel(), join::TYPE_LEFT, $childConfig['field'], $foreignConfig['key']); + } + + // + // Enable virtual field results + // + if (interface_exists('\\codename\\core\\model\\virtualFieldResultInterface') && $this->getMyModel() instanceof virtualFieldResultInterface) { + $this->getMyModel()->setVirtualFieldResult(true); + } + } elseif ($childConfig['type'] === 'collection') { + // Collection, not crud. + + // get the collection config + $collectionConfig = $this->model->config->get('collection>' . $child); + // get the respective model + $childModel = $this->getModel($collectionConfig['model'], $collectionConfig['app'] ?? '', $collectionConfig['vendor'] ?? ''); + + $this->getMyModel()->addCollectionModel($childModel, $child); + + // + // Enable virtual field results + // + if (interface_exists('\\codename\\core\\model\\virtualFieldResultInterface') && $this->getMyModel() instanceof virtualFieldResultInterface) { + $this->getMyModel()->setVirtualFieldResult(true); + } + } + } else { + throw new exception(self::EXCEPTION_CRUD_CHILDREN_CONFIG_MODEL_CONFIG_CHILDREN_IS_NULL, exception::$ERRORLEVEL_ERROR, $child); + } + } + } + } /** - * russian function - * limits field output to visibleFields & optionally: "internalFields" in crud config - * @return void + * returns the request data + * used for normalization + * in form-related functions + * @return array */ - public function limitFieldOutput() { - $visibleFields = $this->getConfig()->get('visibleFields'); - $internalFields = $this->getConfig()->get('internalFields') ?? []; - $this->model->hideAllFields()->addField(implode(',', array_merge($visibleFields, $internalFields))); - foreach($this->childCruds as $crud) { - $crud->limitFieldOutput(); - } + protected function getFormNormalizationData(): array + { + if ($this->formNormalizationData == null) { + $this->setFormNormalizationData($this->getRequest()->getData()); + } + return $this->formNormalizationData; } /** - * reads config from the 'children' key - * and creates instances for those children (cruds) + * sets the underlying data used during normalization + * in the normal use case; this is the pure request data + * @param array $data [description] */ - protected function setChildCruds() { - // apply nested children config - if($this->config->exists('children')) { - foreach($this->config->get('children') as $child) { - - // - // the master child configuration - // in the model - // - $childConfig = $this->model->config->get('children>'.$child); - - // - // optional crud/form overrides - // - $childCrudConfig = $this->config->get('children_config>'.$child); - // DEBUG: \codename\core\app::getResponse()->setData('debug_crud_setchildren_'.$child, $childCrudConfig); - - if($childConfig != null) { - // we handle a single-ref foreign key field as base - // for a nested model as a virtual object key - if($childConfig['type'] == 'foreign') { - - // get the foreign key config - $foreignConfig = $this->model->config->get('foreign>'.$childConfig['field']); - // get the respective model - $childModel = $this->getModel($foreignConfig['model'], $foreignConfig['app'] ?? '', $foreignConfig['vendor'] ?? ''); - // build a child crud - $crud = new \codename\core\ui\crud($childModel, $this->getFormNormalizationData()[$child] ?? []); - - // - // Handle optional configs - // - if(isset($childCrudConfig['crud'])) { - // DEBUG: \codename\core\app::getResponse()->setData('debug_crud_setchildren_'.$child.'_crud', $childCrudConfig['crud']); - $crud->setConfig($childCrudConfig['crud']); - } - if(isset($childCrudConfig['form'])) { - // DEBUG: \codename\core\app::getResponse()->setData('debug_crud_setchildren_'.$child.'_form', $childCrudConfig['form']); - $crud->useForm($childCrudConfig['form']); - } - - // make only a part of the request visible to the crud instance - // DEBUG: $this->getResponse()->setData('debug_crud_'.$child, $this->getFormNormalizationData()[$child] ?? []); - $crud->setFormNormalizationData($this->getFormNormalizationData()[$child] ?? []); - - // store it for later - $this->childCruds[$child] = $crud; - - // - // CHANGED/FEATURE force_virtual_join 2020-07-21 - // This method uses the new feature of the core framework - // this allows virtualizing the table join - // to avoid RDBMS join limitations by abstracting the whole thing. - // - // You have to enable 'force_virtual_join' in the respective model child config - // Cruds enable this feature automatically, while you may opt-in into its usage - // when querying models regularly. - // - if($childConfig['force_virtual_join'] ?? false) { - $virtualJoinModel = $crud->getMyModel(); - $virtualJoinModel->setForceVirtualJoin(true); - $this->getMyModel()->addModel($virtualJoinModel, \codename\core\model\plugin\join::TYPE_LEFT, $childConfig['field'], $foreignConfig['key']); - } else { - // join the model upon the current - $this->getMyModel()->addModel($crud->getMyModel(), \codename\core\model\plugin\join::TYPE_LEFT, $childConfig['field'], $foreignConfig['key']); - } - - // - // Enable virtual field results - // - if(interface_exists('\\codename\\core\\model\\virtualFieldResultInterface') && $this->getMyModel() instanceof \codename\core\model\virtualFieldResultInterface) { - $this->getMyModel()->setVirtualFieldResult(true); - } - } else if($childConfig['type'] === 'collection') { - - // Collection, not a crud. - - // get the collection config - $collectionConfig = $this->model->config->get('collection>'.$child); - // get the respective model - $childModel = $this->getModel($collectionConfig['model'], $collectionConfig['app'] ?? '', $collectionConfig['vendor'] ?? ''); - - $this->getMyModel()->addCollectionModel($childModel, $child); - - // - // Enable virtual field results - // - if(interface_exists('\\codename\\core\\model\\virtualFieldResultInterface') && $this->getMyModel() instanceof \codename\core\model\virtualFieldResultInterface) { - $this->getMyModel()->setVirtualFieldResult(true); - } - } - } else { - throw new exception(self::EXCEPTION_CRUD_CHILDREN_CONFIG_MODEL_CONFIG_CHILDREN_IS_NULL, exception::$ERRORLEVEL_ERROR, $child); - } - } - } + public function setFormNormalizationData(array $data): void + { + $this->formNormalizationData = $data; } /** - * exception thrown if a given model does not define child configuration - * but crud tries to use it - * @var string + * access a data array currently used by this crud + * @param string $key [description] + * @return mixed */ - const EXCEPTION_CRUD_CHILDREN_CONFIG_MODEL_CONFIG_CHILDREN_IS_NULL = 'EXCEPTION_CRUD_CHILDREN_CONFIG_MODEL_CONFIG_CHILDREN_IS_NULL'; + public function getData(string $key = ''): mixed + { + return $this->data?->getData($key); + } /** - * loads the crud config - * defaults to schema_table, if no identifier (or '') is specified - * @param string $identifier [description] - * @return \codename\core\config [description] - */ - protected function loadConfig(string $identifier = '') : \codename\core\config{ - if($identifier == '') { - $identifier = $this->getMyModel()->schema . '_' . $this->getMyModel()->table; - } - - // prepare config - $config = null; - - // - // Try to retrieve cached config - // - if($this->useConfigCache) { - $cacheGroup = app::getVendor().'_'.app::getApp().'_CRUD_CONFIG'; - $cacheKey = $identifier; - if($cachedConfig = \codename\core\app::getCache()->get($cacheGroup, $cacheKey)) { - $config = new \codename\core\config($cachedConfig); - } - } + * Loads data from a form configuration file + * @param string $identifier + * @return crud + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + * @todo USE CACHE FOR CONFIGS + */ + public function useForm(string $identifier): crud + { + $this->getForm()->setId($identifier); - // - // If config not already set by cache, get it - // - if(!$config) { - $config = new \codename\core\config\json('config/crud/' . $identifier . '.json', true); + $formConfig = $this->loadFormConfig($identifier); - // Cache, if enabled. - if($this->useConfigCache) { - \codename\core\app::getCache()->set($cacheGroup, $cacheKey, $config->get()); + // + // update child crud configs + // + if ($formConfig->exists('children_config')) { + $childrenConfig = $formConfig->get('children_config'); + foreach ($childrenConfig as $childName => $childConfig) { + if (isset($this->childCruds[$childName])) { + if (isset($childConfig['crud'])) { + $this->childCruds[$childName]->setConfig($childConfig['crud']); + } + if (isset($childConfig['form'])) { + $this->childCruds[$childName]->useForm($childConfig['form']); + } + } + } } - } - return $config; - } + if ($formConfig->exists('tag')) { + $this->getForm()->config['form_tag'] = $formConfig->get('tag'); + } - /** - * Cache configurations - * @var bool - */ - protected $useConfigCache = true; + if ($formConfig->exists('fieldset')) { + foreach ($formConfig->get('fieldset') as $key => $fieldset) { + $newFieldset = new fieldset(['fieldset_name' => $key]); + foreach ($formConfig->get('fieldset>' . $key . '>field') as $field) { + $options = []; + $options['field_required'] = ($formConfig->exists('fieldset>' . $key . '>required') && in_array($field, $formConfig->get('fieldset>' . $key . '>required'))); + $options['field_readonly'] = ($formConfig->exists('fieldset>' . $key . '>readonly') && in_array($field, $formConfig->get('fieldset>' . $key . '>readonly'))); + // + // CHANGED 2021-10-27: flag fields in fieldsets now use the same type/handling as root-level flag fields + // + if ($field == $this->getMyModel()->table . '_flag') { + $flags = $this->getMyModel()->config->get('flag'); + if (!is_array($flags)) { + continue; + } - /** - * Enable/disable caching of the crud config - * @param bool $state [description] - * @return crud [description] - */ - public function setConfigCache(bool $state) : crud { - $this->useConfigCache = $state; - return $this; - } + $value = []; + $elements = []; - /** - * set config by identifier - * @param string $identifier [description] - * @return \codename\core\ui\crud [description] - */ - public function setConfig(string $identifier = '') : \codename\core\ui\crud { - $this->config = $this->loadConfig($identifier); + foreach ($flags as $flagname => $flag) { + $value[$flagname] = !is_null($this->data) && $this->getMyModel()->isFlag($flag, $this->data->getData()); + $elements[] = [ + 'name' => $flagname, + 'display' => app::getTranslate()->translate('DATAFIELD.' . $field . '_' . $flagname), + 'value' => $flag, + ]; + } + + $fielddata = [ + 'field_name' => $this->getMyModel()->table . '_flag', + 'field_type' => 'multicheckbox', + 'field_datatype' => 'structure', + 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $field), + 'field_multiple' => true, + 'field_value' => $value, + 'field_elements' => $elements, + 'field_idfield' => 'name', + 'field_displayfield' => '{$element["display"]}', // todo: translate! + 'field_valuefield' => 'value', + ]; + + if ($this->readOnly + || ($this->config->exists('readonly') && is_array($this->config->get('readonly')) && in_array($field, $this->config->get('readonly'))) + || $options['field_readonly'] + ) { + $fielddata['field_readonly'] = true; + } - $this->customizedFields = $this->config->get('customized_fields') ?? []; + if ($options['field_required'] ?? false) { + $fielddata['field_required'] = true; + } - if($identifier !== '') { - $this->updateChildCrudConfigs(); - } + $c = &$this->onCreateFormfield; + if ($c !== null && is_callable($c)) { + $c($fielddata); + } + + $formField = new field($fielddata); + + $c = &$this->onFormfieldCreated; + if ($c !== null && is_callable($c)) { + $c($formField); + } + + $newFieldset->addField($formField); + continue; + } + + $newFieldset->addField($this->makeField($field, $options)); + } + $this->getForm()->addFieldset($newFieldset); + } + } elseif ($formConfig->exists("field")) { + $this->fields = $formConfig->get("field"); + $this->fieldsformConfig = []; + if ($formConfig->exists('required')) { + $this->fieldsformConfig['required'] = $formConfig->get('required'); + } + if ($formConfig->exists('readonly')) { + $this->fieldsformConfig['readonly'] = $formConfig->get('readonly'); + } + } - return $this; + return $this; } /** - * [setCustomizedFields description] - * @param array $fields [description] + * Returns the form instance of this CRUD generator instance + * @return form */ - public function setCustomizedFields(array $fields) { - $this->customizedFields = $fields; + public function getForm(): form + { + return $this->form; } /** - * [updateChildCrudConfigs description] - * @return [type] [description] + * [loadFormConfig description] + * @param string $identifier [description] + * @return config + * @throws ReflectionException + * @throws exception */ - protected function updateChildCrudConfigs() { - if($this->config->exists('children_config')) { - $childrenConfig = $this->config->get('children_config'); - foreach($childrenConfig as $childName => $childConfig) { - if(isset($this->childCruds[$childName])) { - if(isset($childConfig['crud'])) { - $this->childCruds[$childName]->setConfig($childConfig['crud']); + protected function loadFormConfig(string $identifier): config + { + // prepare config + $config = null; + $cacheGroup = app::getVendor() . '_' . app::getApp() . '_CRUD_FORM'; + $cacheKey = $identifier; + + // + // Try to retrieve cached config + // + if ($this->useConfigCache) { + if ($cachedConfig = app::getCache()->get($cacheGroup, $cacheKey)) { + $config = new config($cachedConfig); } - if(isset($childConfig['form'])) { - $this->childCruds[$childName]->useForm($childConfig['form']); + } + + // + // If config not already set by cache, get it + // + if (!$config) { + $config = new json('config/crud/form_' . $identifier . '.json'); + + // Cache, if enabled. + if ($this->useConfigCache) { + app::getCache()->set($cacheGroup, $cacheKey, $config->get()); } - } } - } - } - /** - * access data array currently used by this crud - * @param string $key [description] - * @return array|null|mixed - */ - public function getData(string $key = '') { - if($this->data !== null) { - return $this->data->getData($key); - } else { - return null; - } + return $config; } /** - * I will add a modifier for a field. - *
Modifiers are used to change the output of a field in the CRUD list. - * @example addModifier('status_id', function($row) {return ''.$row['status_name'].'';}); - * @param string $field - * @param callable $modifier - * @return \codename\core\ui\crud + * Returns the private model of this instance + * @return model */ - public function addModifier(string $field, callable $modifier) : \codename\core\ui\crud { - $this->modifiers[$field] = $modifier; - return $this; + public function getMyModel(): model + { + return $this->model; } /** - * [protected description] - * @var callable[] + * Creates the field instance for the given field and adds information to it. + * @param string $field [description] + * @param array $options [description] + * @return field + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception */ - protected $resultsetModifiers = []; + public function makeField(string $field, array $options = []): field + { + // load model config for simplicity + $modelconfig = $this->getMyModel()->config->get(); - /** - * add a modifier for modifying the whole resultset - * @param callable $modifier [description] - * @return \codename\core\ui\crud [description] - */ - public function addResultsetModifier(callable $modifier) : \codename\core\ui\crud { - $this->resultsetModifiers[] = $modifier; - return $this; - } + // Error if field not in models + if (!in_array($field, $this->getMyModel()->getFields())) { + throw new exception(self::EXCEPTION_MAKEFIELD_FIELDNOTFOUNDINMODEL, exception::$ERRORLEVEL_ERROR, $field); + } - /** - * default column ordering - * @var string[] - */ - protected $columnOrder = array(); + // Create a basic formfield array + $fielddata = [ + 'field_id' => $field, + 'field_name' => $field, + 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $field), + 'field_description' => app::getTranslate()->translate('DATAFIELD.' . $field . '_DESCRIPTION'), + 'field_type' => 'input', + 'field_required' => $options['field_required'] ?? false, + 'field_placeholder' => app::getTranslate()->translate('DATAFIELD.' . $field), + 'field_multiple' => false, + 'field_readonly' => $options['field_readonly'] ?? false, + ]; - /** - * configures column ordering for crud - * @param array $columns [description] - * @return \codename\core\ui\crud - */ - public function setColumnOrder(array $columns = array()) : \codename\core\ui\crud { - $this->columnOrder = $columns; - return $this; - } + // Get the displaytype of this field + if (array_key_exists('datatype', $modelconfig) && array_key_exists($field, $modelconfig['datatype'])) { + $fielddata['field_type'] = $this->getDisplaytype($modelconfig['datatype'][$field]); + $fielddata['field_datatype'] = $modelconfig['datatype'][$field]; + } - /** - * I will add a ROW modifier - * which can change the output elements attributes - * @example addModifier('status_id', function($row) {return ''.$row['status_name'].'';}); - * @param callable $modifier [description] - * @return \codename\core\ui\crud [description] - */ - public function addRowModifier(callable $modifier) : \codename\core\ui\crud { - $this->rowModifiers[] = $modifier; - return $this; - } + if ($fielddata['field_type'] == 'yesno') { + $fielddata['field_type'] = 'select'; + $fielddata['field_displayfield'] = '{$element[\'field_name\']}'; + $fielddata['field_valuefield'] = 'field_value'; - /** - * Returns the config for listview() - * @return void - */ - public function listconfig() { - $visibleFields = $this->config->get('visibleFields'); - - // Only append primarykey, if not added to visibleFields - if(!in_array($this->getMyModel()->getPrimarykey(), $visibleFields)) { - $visibleFields[] = $this->getMyModel()->getPrimarykey(); - } - - $formattedFields = []; - - // - // Format foreign key values as defined by the model - // - if(!is_null($this->getMyModel()->config->get('foreign'))) { - $foreignKeys = $this->getMyModel()->config->get('foreign'); - - $formattedFields = array_reduce(array_keys($foreignKeys), function ($carry, $key) { - // foreign keys use a formatted output field AND a data key - $carry[$key] = $key.'_FORMATTED'; - return $carry; - }, $formattedFields); - } - - // - // also include "modifier" fields as _FORMATTED ones. - // - $formattedFields = array_merge($formattedFields, array_reduce(array_keys($this->modifiers), function ($carry, $key) { - // use modifier key as final field - $carry[$key] = $key; - return $carry; - }, [])); - - // - // Fields that are available as raw data AND as a _FORMATTED one - // - $this->getResponse()->setData('formattedFields', $formattedFields); - - // - // Enable custom selection of displayed fields (columns) - // - $this->getResponse()->setData('enable_displayfieldselection', ($this->config->exists('displayFieldSelection') ? $this->config->get('displayFieldSelection') : false)); - - if($this->config->exists('availableFields')) { - $availableFields = $this->config->get('availableFields'); - } else { - // enable ALL fields of the model to be displayed - $availableFields = $this->getMyModel()->config->get('field'); - } - - // add formatted fields to availableFields - $availableFields = array_merge($availableFields, array_keys($formattedFields)); - - // remove all disabled fields - if($this->config->exists('disabled')) { - $availableFields = array_diff($availableFields, $this->config->get('disabled')); - } - - // merge and kill duplicates - $availableFields = array_values(array_unique($availableFields)); - - $displayFields = array(); - - // display fields are either the visibleFields (defined in config) or submitted - // in the latter case, we have to check for legitimacy first. - if($this->getRequest()->isDefined('display_selectedfields') && $this->getResponse()->getData('enable_displayfieldselection') == true) { - $selectedFields = $this->getRequest()->getData('display_selectedfields'); - if(is_array($selectedFields)) { - // - // NOTE/CHANGED 2019-06-14: we have to include some more fields - // - $avFields = array_unique(array_merge($visibleFields, $availableFields)); - foreach($selectedFields as $displayField) { - if(in_array($displayField, $avFields)) { - $displayFields[] = $displayField; - } - } - } - } - - if(count($displayFields) > 0) { - $visibleFields = $displayFields; - } else { - // add all modifier fields by default - // if no field selection provided - $visibleFields = array_merge($visibleFields, array_keys($this->modifiers)); - } - - if(!in_array($this->getMyModel()->getPrimarykey(), $visibleFields)) { - $visibleFields[] = $this->getMyModel()->getPrimarykey(); - } - - // - // Provide some labels for frontend display - // - $fieldLabels = []; - foreach(array_merge($availableFields, $visibleFields, $formattedFields) as $field) { - if(!is_string($field)) { continue; } - $fieldLabels[$field] = app::getTranslate()->translate('DATAFIELD.'.$field); - } - foreach($availableFields as $field) { - if($fieldLabels[$field] ?? false) { - $fieldLabels[$field] = app::getTranslate()->translate('DATAFIELD.'.$field); - } - } - $this->getResponse()->setData('labels', $fieldLabels); - if($this->getConfig()->exists('export>_security>group')) { - if($enableExport = app::getAuth()->memberOf($this->getConfig()->get('export>_security>group'))) { - $this->getResponse()->setData('export_types', $this->getConfig()->get('export>allowedTypes')); - } - $this->getResponse()->setData('enable_export', $enableExport); - } else { - $this->getResponse()->setData('enable_export', false); - } - - if($this->getConfig()->exists('import>_security>group')) { - if($enableImport = app::getAuth()->memberOf($this->getConfig()->get('import>_security>group'))) { - // $this->getResponse()->setData('export_types', $this->getConfig()->get('export>allowedTypes')); - } - $this->getResponse()->setData('enable_import', $enableImport); - } else { - $this->getResponse()->setData('enable_import', false); - } - - $fieldActions = $this->config->get("action>field") ?? array(); - $filters = $this->config->get('visibleFilters', array()); - // merge-in provided filters - $filters = array_merge($filters, $this->providedFilters); - - // - // build a form from filters - // - $filterForm = null; - - if(count($filters) > 0) { - $filterForm = new \codename\core\ui\form([ - 'form_id' => 'filterform', - 'form_method' => 'post', - 'form_action' => '' - ]); + // NOTE: Datatype for this kind of pseudo-boolean field must be null or so + // because the boolean validator really needs a bool. + $fielddata['field_datatype'] = null; + $fielddata['field_elements'] = [ + [ + 'field_value' => true, + 'field_name' => 'Ja', + ], + [ + 'field_value' => false, + 'field_name' => 'Nein', + ], + ]; + } - $filterForm->setFormRequest($this->getRequest()->getData(self::CRUD_FILTER_IDENTIFIER) ?? []); + if ($this->config->exists("required")) { + if (in_array($field, $this->config->get('required'))) { + $fielddata['field_required'] = true; + } + } - foreach($filters as $filterSpecifier => $filterConfig) { - $specifier = explode('.', $filterSpecifier); - $useModel = $this->getMyModel(); + if (!is_null($this->data)) { + $fielddata['field_value'] = ($this->data->isDefined($field) ? $this->getMyModel()->exportField(new modelfield($field), $this->data->getData($field)) : null); + } - $fName = $specifier[count($specifier)-1]; + // Set primary key field hidden + if ($field == $this->getMyModel()->getPrimaryKey()) { + $fielddata['field_type'] = 'hidden'; + } - if(count($specifier) == 2) { - // we have a model/table reference - $useModel = $this->getModel($specifier[0]); - } + // Decode object datatype + if (str_contains($fielddata['field_type'], 'bject_')) { + $fielddata['field_value'] = app::object2array(json_decode($fielddata['field_value'])); + } + if ($this->getMyModel()->config->exists("required") && in_array($field, $this->getMyModel()->config->get("required"))) { + $fielddata['field_required'] = true; + } - $field = null; + // Modify field to be a reference dropdown + if (array_key_exists('foreign', $modelconfig) && array_key_exists($field, $modelconfig['foreign'])) { + if (!app::getValidator('structure_config_modelreference')->reset()->isValid($modelconfig['foreign'][$field])) { + throw new exception(self::EXCEPTION_MAKEFIELD_INVALIDREFERENCEOBJECT, exception::$ERRORLEVEL_ERROR, $modelconfig['foreign'][$field]); + } - // field is a foreign key - if(!($filterConfig['wildcard'] ?? false) && in_array($fName, $useModel->config->get('field'))) { - $field = $this->makeFieldForeign($useModel, $fName, $filterConfig); // options? + $foreign = $modelconfig['foreign'][$field]; - // if(is_array($filterForm->getData($filterSpecifier))) { - // // normalize pre-set value differently - // $filterValue = $filterForm->getData($filterSpecifier); - // $elementDatatype = $useModel->getConfig()->get('datatype>'.$fName); - // $filterValue = array_map(function($element) use( $filterSpecifier, $elementDatatype) { - // return \codename\core\ui\field::getNormalizedFieldValue($filterSpecifier, $element, $elementDatatype); - // }, $filterValue); - // $field->setValue($filterValue); - // } else { - // $field->setValue( \codename\core\ui\field::getNormalizedFieldValue($filterSpecifier, $filterForm->getData($filterSpecifier), $field->getProperty('field_datatype')) ); - // } + $elements = $this->getModel($foreign['model'], $foreign['app'] ?? app::getApp()); - } elseif($filterConfig['config']['field_config'] ?? false) { - $fieldData = array_merge( - [ - 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $fName), - 'field_name' => $filterSpecifier, - 'field_type' => 'input', - ], - $filterConfig['config']['field_config'] - ); - $field = new \codename\core\ui\field($fieldData); - } else { - // wildcard, no normalization needed - $field = new \codename\core\ui\field([ - 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $fName), - 'field_name' => $filterSpecifier, - 'field_type' => 'input', - // 'field_value' => $filterForm->getData($filterSpecifier) - ]); - } - - $filterForm->addField($field); - } - } - - if(count($this->columnOrder) > 0) { - $visibleFields = array_values(array_unique(array_merge(array_intersect($this->columnOrder, $visibleFields), $visibleFields), SORT_REGULAR)); - } else { - $visibleFields = array_values(array_unique($visibleFields, SORT_REGULAR)); - } - $this->getResponse()->setData('filterform', $filterForm ? $filterForm->output(true) : null); - - $this->getResponse()->setData('topActions', $this->prepareActionsOutput($this->config->get("action>top") ?? [])); - $this->getResponse()->setData('bulkActions', $this->prepareActionsOutput($this->config->get("action>bulk") ?? [])); - $this->getResponse()->setData('elementActions', $this->prepareActionsOutput($this->config->get("action>element") ?? [])); - $this->getResponse()->setData('fieldActions', $this->prepareActionsOutput($fieldActions) ?? []); - $this->getResponse()->setData('visibleFields', $visibleFields); - $this->getResponse()->setData('availableFields', $availableFields); - $this->getResponse()->setData('crud_filter_identifier', self::CRUD_FILTER_IDENTIFIER); - $this->getResponse()->setData('filters_used', $filterForm ? $filterForm->normalizeData($filterForm->getData()) : null); - $this->getResponse()->setData('enable_search_bar', $this->config->exists("visibleFilters>_search")); - $this->getResponse()->setData('modelinstance', $this->getMyModel()); - - return; - } + // + // skip the basic model setup if we're using the remote api interface anyway. + // + if (!($elements instanceof exposesRemoteApiInterface) || !isset($foreign['remote_source'])) { + if (array_key_exists('order', $foreign) && is_array($foreign['order'])) { + foreach ($foreign['order'] as $order) { + if (!app::getValidator('structure_config_modelorder')->reset()->isValid($order)) { + throw new exception(self::EXCEPTION_MAKEFIELD_INVALIDORDEROBJECT, exception::$ERRORLEVEL_ERROR, $order); + } + $elements->addOrder($order['field'], $order['direction']); + } + } - /** - * Returns a list of the entries in the model and paginate, filter and order it - * @return void - */ - public function listview() { - $visibleFields = $this->config->get('visibleFields'); + if (array_key_exists('filter', $foreign) && is_array($foreign['filter'])) { + foreach ($foreign['filter'] as $filter) { + if (!app::getValidator('structure_config_modelfilter')->reset()->isValid($filter)) { + throw new exception(self::EXCEPTION_MAKEFIELD_INVALIDFILTEROBJECT, exception::$ERRORLEVEL_ERROR, $filter); + } + if ($filter['field'] == $elements->getIdentifier() . '_flag') { + if ($filter['operator'] == '=') { + $elements->withFlag($elements->config->get('flag>' . $filter['value'])); + } elseif ($filter['operator'] == '!=') { + $elements->withoutFlag($elements->config->get('flag>' . $filter['value'])); + } else { + throw new exception(self::EXCEPTION_MAKEFIELD_FILTER_FLAG_INVALIDOPERATOR, exception::$ERRORLEVEL_ERROR, $filter); + } + } else { + $elements->addFilter($filter['field'], $filter['value'], $filter['operator']); + } + } + } + } - // Only append primarykey, if not added to visibleFields - if(!in_array($this->getMyModel()->getPrimarykey(), $visibleFields)) { - $visibleFields[] = $this->getMyModel()->getPrimarykey(); - } + $fielddata['field_type'] = 'select'; + $fielddata['field_displayfield'] = $foreign['display']; + $fielddata['field_valuefield'] = $foreign['key']; - $formattedFields = []; + if ($elements instanceof exposesRemoteApiInterface && isset($foreign['remote_source'])) { + $apiEndpoint = $elements->getExposedApiEndpoint(); + $fielddata['field_remote_source'] = $apiEndpoint; - // - // Format foreign key values as defined by the model - // - if(!is_null($this->getMyModel()->config->get('foreign'))) { - $foreignKeys = $this->getMyModel()->config->get('foreign'); + $remoteSource = $foreign['remote_source']; - $formattedFields = array_reduce(array_keys($foreignKeys), function ($carry, $key) { - // foreign keys use a formatted output field AND a data key - $carry[$key] = $key.'_FORMATTED'; - return $carry; - }, $formattedFields); - } + $filterKeys = []; + foreach ($remoteSource['filter_key'] as $filterKey => $filterData) { + if (is_array($filterData)) { + foreach ($filterData as $filterDataData) { + $filterKeys[$filterKey][$filterDataData] = true; + } + } else { + $filterKeys[$filterData] = true; + } + } - // - // also include "modifier" fields as _FORMATTED ones. - // - $formattedFields = array_merge($formattedFields, array_reduce(array_keys($this->modifiers), function ($carry, $key) { - // use modifier key as final field - $carry[$key] = $key; - return $carry; - }, [])); + $fielddata['field_remote_source_filter_key'] = $filterKeys; - // - // Fields that are available as raw data AND as a _FORMATTED one - // - $this->getResponse()->setData('formattedFields', $formattedFields); + // + // Explicit Filter Key + // for retrieving an already set, unique and strictly defined value + // + if ($remoteSource['explicit_filter_key'] ?? false) { + $fielddata['field_remote_source_explicit_filter_key'] = $remoteSource['explicit_filter_key']; + } - // - // Enable custom selection of displayed fields (columns) - // - $this->getResponse()->setData('enable_displayfieldselection', ($this->config->exists('displayFieldSelection') ? $this->config->get('displayFieldSelection') : false)); + $fielddata['field_remote_source_parameter'] = $remoteSource['parameters'] ?? []; + $fielddata['field_remote_source_display_key'] = $remoteSource['display_key'] ?? null; + $fielddata['field_remote_source_links'] = $foreign['remote_source']['links'] ?? []; + $fielddata['field_valuefield'] = $foreign['key']; + $fielddata['field_displayfield'] = $foreign['key']; // $defaultDisplayField[$foreign['model']] ?? $foreign['key']; + } elseif (!in_array($field, $this->customizedFields)) { + $fielddata['field_elements'] = $elements->search()->getResult(); + } - if($this->config->exists('availableFields')) { - $availableFields = $this->config->get('availableFields'); - } else { - // enable ALL fields of the model to be displayed - $availableFields = $this->getMyModel()->config->get('field'); + if (array_key_exists('datatype', $modelconfig) && array_key_exists($field, $modelconfig['datatype']) && $modelconfig['datatype'][$field] == 'structure') { + $fielddata['field_multiple'] = true; + } } - // add formatted fields to availableFields - $availableFields = array_merge($availableFields, array_keys($formattedFields)); - - // remove all disabled fields - if($this->config->exists('disabled')) { - $availableFields = array_diff($availableFields, $this->config->get('disabled')); - } + // + // nested crud / submodel + // + if ($this->config->exists('children') && in_array($field, $this->config->get('children'))) { + $childConfig = $this->model->config->get('children>' . $field); + + if ($childConfig['type'] === 'foreign') { + // + // Handle nested forms + // + $fielddata['field_type'] = 'form'; + + // provide a sub-form config ! + $crud = $this->childCruds[$field]; + $crud->onCreateFormfield = $this->onCreateFormfield; + $crud->onFormfieldCreated = $this->onFormfieldCreated; + + if ($this->readOnly) { + $crud->readOnly = $this->readOnly; + } - // merge and kill duplicates - $availableFields = array_values(array_unique($availableFields)); + // available child config keys: + // - type (e.g., foreign) + // - field (reference field) + $childIdentifierValue = ($this->data && $this->data->isDefined($childConfig['field']) ? $this->getMyModel()->exportField(new modelfield($childConfig['field']), $this->data->getData($childConfig['field'])) : null); + $form = $crud->makeForm($childIdentifierValue, false); // make form without submitting - $displayFields = array(); + $fielddata['form'] = $form; + $formdata = []; + foreach ($form->getFields() as $field) { + $formdata[$field->getConfig()->get('field_name')] = $field->getConfig()->get('field_value'); + } + $fielddata['field_value'] = $formdata; + } elseif ($childConfig['type'] === 'collection') { + // + // Handle collections + // + $collectionConfig = $this->model->config->get('collection>' . $field); + $fielddata['field_type'] = 'table'; + $fielddata['field_datatype'] = 'structure'; + + $crud = new crud($this->getModel($collectionConfig['model'], $collectionConfig['app'] ?? '', $collectionConfig['vendor'] ?? '')); + $crud->onCreateFormfield = $this->onCreateFormfield; + $crud->onFormfieldCreated = $this->onFormfieldCreated; + // TODO: allow custom crud config somehow? + // $crud->setConfig('some-crud-config'); + + $fielddata['field_rowkey'] = $crud->getMyModel()->getPrimaryKey(); + + $fielddata['visibleFields'] = $crud->getConfig()->get('visibleFields'); + + $fielddata['labels'] = []; + foreach ($fielddata['visibleFields'] as $field) { + $fielddata['labels'][$field] = app::getTranslate()->translate('DATAFIELD.' . $field); + } - // display fields are either the visibleFields (defined in config) or submitted - // in the latter case, we have to check for legitimacy first. - if($this->getRequest()->isDefined('display_selectedfields') && $this->getResponse()->getData('enable_displayfieldselection') == true) { - $selectedFields = $this->getRequest()->getData('display_selectedfields'); - if(is_array($selectedFields)) { - // - // NOTE/CHANGED 2019-06-14: we have to include some more fields - // - $avFields = array_unique(array_merge($visibleFields, $availableFields)); - foreach($selectedFields as $displayField) { - if(in_array($displayField, $avFields)) { - $displayFields[] = $displayField; - } + $form = $crud->makeForm(null, false); + $fielddata['form'] = $form->output(true); } - } } - if(count($displayFields) > 0) { - $visibleFields = $displayFields; - } else { - // add all modifier fields by default - // if no field selection provided - $visibleFields = array_merge($visibleFields, array_keys($this->modifiers)); + if ($this->readOnly) { + $fielddata['field_readonly'] = true; } - if(!in_array($this->getMyModel()->getPrimarykey(), $visibleFields)) { - $visibleFields[] = $this->getMyModel()->getPrimarykey(); + $c = &$this->onCreateFormfield; + if ($c !== null && is_callable($c)) { + $c($fielddata); } - // - // Provide some labels for frontend display - // - $fieldLabels = []; - foreach(array_merge($availableFields, $visibleFields, $formattedFields) as $field) { - if(!is_string($field)) { continue; } - $fieldLabels[$field] = app::getTranslate()->translate('DATAFIELD.'.$field); - } - foreach($availableFields as $field) { - if($fieldLabels[$field] ?? false) { - $fieldLabels[$field] = app::getTranslate()->translate('DATAFIELD.'.$field); - } + $field = new field($fielddata); + $field->setType('compact'); + + $c = &$this->onFormfieldCreated; + if ($c !== null && is_callable($c)) { + $c($field); } - $this->getResponse()->setData('labels', $fieldLabels); - // - // NOTE: CHANGED on 2018-08-31 - // If we explicitly add fields here - // the model may rush into a severe result normalization situation. - // - // $this->getMyModel()->addField(implode(',', $visibleFields)); - // + // Add the field to the form + return $field; + } - $this->applyFilters(); + /** + * Resolve a datatype to a forced display type + * @param string $datatype + * @return string + */ + public function getDisplaytype(string $datatype): string + { + return self::getDisplaytypeStatic($datatype); + } - if($this->allowPagination) { - $this->makePagination(); - } + /** + * [getDisplaytypeStatic description] + * @param string $datatype [description] + * @return string [description] + */ + public static function getDisplaytypeStatic(string $datatype): string + { + return match ($datatype) { + 'structure_address' => 'structure_address', + 'structure_text_telephone' => 'structure_text_telephone', + 'structure' => 'structure', + 'boolean' => 'yesno', + 'text_date', 'text_date_birthdate' => 'date', + 'text_timestamp' => 'timestamp', + // + // CHANGED 2020-05-26: relativetime field detection/determination + // moved to here from field class + // + 'text_datetime_relative' => 'relativetime', + default => 'input', + }; + } - if($this->crudSeekOverridePkeyOrder) { - // Seek-mode order hack - // following ordering happends during runtime below - $this->getMyModel()->addOrder($this->getMyModel()->getPrimarykey(), $this->crudSeekOverridePkeyOrder); - } + /** + * Adds the important fields to the form instance of this crud editor + * + * @param string|null $primarykey [primary key of the entry to be used as value base or null] + * @param bool $addSubmitButton [whether the form should add a submitted button field by default] + * @return form [the form (also contained in this crud instance, accessible via ->getForm())] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function makeForm(null|string $primarykey = null, bool $addSubmitButton = true): form + { + $this->useEntry($primarykey); - foreach ($this->config->get("order") as $order) { - $this->getMyModel()->addOrder($order['field'], $order['direction']); - } + // set request data only visible for this form + // usually, this is the complete request, + // but it may only be a part of it. + $this->getForm()->setFormRequest($this->getFormNormalizationData()); + if ($this->config->exists('tag')) { + $this->getForm()->config['form_tag'] = $this->config->get('tag'); + } - if($this->getConfig()->exists('export>_security>group')) { - if($enableExport = app::getAuth()->memberOf($this->getConfig()->get('export>_security>group'))) { - $this->getResponse()->setData('export_types', $this->getConfig()->get('export>allowedTypes')); - } - $this->getResponse()->setData('enable_export', $enableExport); - } else { - $this->getResponse()->setData('enable_export', false); + // use "field", if defined in crud config + if ($this->config->exists('field') && count($this->fields) === 0) { + $this->fields = $this->config->get('field'); } - if($this->getConfig()->exists('import>_security>group')) { - if($enableImport = app::getAuth()->memberOf($this->getConfig()->get('import>_security>group'))) { - // $this->getResponse()->setData('export_types', $this->getConfig()->get('export>allowedTypes')); - } - $this->getResponse()->setData('enable_import', $enableImport); - } else { - $this->getResponse()->setData('enable_import', false); + if (count($this->fields) == 0 && count($this->getForm()->getFieldsets()) == 0) { + $this->fields = $this->getMyModel()->config->get('field'); } - $fieldActions = $this->config->get("action>field") ?? array(); - $filters = $this->config->get('visibleFilters', array()); - // merge-in provided filters - $filters = array_merge($filters, $this->providedFilters); + // Be sure to show the primary key in the form + $this->fields[] = $this->getMyModel()->getPrimaryKey(); + $this->fields = array_unique($this->fields); - // - // build a form from filters - // - $filterForm = null; + foreach ($this->fields as $field) { + if ($this->config->exists('disabled') && is_array($this->config->get('disabled')) && in_array($field, $this->config->get('disabled'))) { + continue; + } - if(count($filters) > 0) { - $filterForm = new \codename\core\ui\form([ - 'form_id' => 'filterform', - 'form_method' => 'post', - 'form_action' => '' - ]); - - $filterForm->setFormRequest($this->getRequest()->getData(self::CRUD_FILTER_IDENTIFIER) ?? []); - - foreach($filters as $filterSpecifier => $filterConfig) { - $specifier = explode('.', $filterSpecifier); - $useModel = $this->getMyModel(); - - $fName = $specifier[count($specifier)-1]; - - if(count($specifier) == 2) { - // we have a model/table reference - $useModel = $this->getModel($specifier[0]); - } - - $field = null; - - // field is a foreign key - if(!($filterConfig['wildcard'] ?? false) && in_array($fName, $useModel->config->get('field'))) { - $field = $this->makeFieldForeign($useModel, $fName, $filterConfig); // options? - - // if(is_array($filterForm->getData($filterSpecifier))) { - // // normalize pre-set value differently - // $filterValue = $filterForm->getData($filterSpecifier); - // $elementDatatype = $useModel->getConfig()->get('datatype>'.$fName); - // $filterValue = array_map(function($element) use( $filterSpecifier, $elementDatatype) { - // return \codename\core\ui\field::getNormalizedFieldValue($filterSpecifier, $element, $elementDatatype); - // }, $filterValue); - // $field->setValue($filterValue); - // } else { - // $field->setValue( \codename\core\ui\field::getNormalizedFieldValue($filterSpecifier, $filterForm->getData($filterSpecifier), $field->getProperty('field_datatype')) ); - // } - - } elseif($filterConfig['config']['field_config'] ?? false) { - $fieldData = array_merge( - [ - 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $fName), - 'field_name' => $filterSpecifier, - 'field_type' => 'input', - ], - $filterConfig['config']['field_config'] - ); - $field = new \codename\core\ui\field($fieldData); - } else { - // wildcard, no normalization needed - $field = new \codename\core\ui\field([ - 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $fName), - 'field_name' => $filterSpecifier, - 'field_type' => 'input', - // 'field_value' => $filterForm->getData($filterSpecifier) - ]); + if (in_array($field, [$this->getMyModel()->table . "_modified", $this->getMyModel()->table . "_created"])) { + continue; + } + if (!in_array($field, $this->getMyModel()->config->get('field'))) { + throw new exception(self::EXCEPTION_MAKEFORM_FIELDNOTFOUNDINMODEL, exception::$ERRORLEVEL_ERROR, $field); } - $filterForm->addField($field); - } - } + // exclude child model fields that have an active children config for this crud + if ($this->config->exists('children') && $this->getMyModel()->config->exists('children')) { + // if a field exists in a child config field reference + $found = false; + foreach ($this->config->get('children') as $childField) { + if (($childConfig = $this->getMyModel()->config->get('children>' . $childField)) !== null) { + if ($childConfig['type'] === 'foreign' && $childConfig['field'] == $field) { + $found = true; + break; + } elseif ($childField === $field && $childConfig['type'] === 'collection') { + // $found = true; + break; + } + } + } + if ($found) { + continue; + } + } + if ($field == $this->getMyModel()->table . '_flag') { + $flags = $this->getMyModel()->config->get('flag'); + if (!is_array($flags)) { + continue; + } - // foreach($filters as $tFName => &$fData) { - // - // $specifier = explode('.', $tFName); - // $useModel = $this->getMyModel(); - // - // $fName = $specifier[count($specifier)-1]; - // - // if(count($specifier) == 2) { - // // we have a model/table reference - // $useModel = $this->getModel($specifier[0]); - // } - // - // // handle date_range filter. - // // @TODO: create a more general UI-specific function that returns datatype-specific fields/filter-UI elements - // if(in_array($useModel->config->get('datatype>'.$fName), array('text_timestamp', 'text_date'))) { - // $fData['filtertype'] = 'date_range'; - // } - // - // // field is a foreign key - // if($fData['wildcard'] == false && $useModel->config->exists('foreign>'.$fName)) { - // // modify filter, add filteroptions - // $fConfig = $useModel->config->get('foreign>'.$fName); - // $fModel = $this->getModel($fConfig['model']); - // if(isset($fConfig['filter'])) { - // foreach($fConfig['filter'] as $modelFilter) { - // $fModel->addFilter($modelFilter['field'],$modelFilter['value'],$modelFilter['operator']); - // } - // } - // $fResult = $fModel->search()->getResult(); - // $fData['filteroptions'] = array(); - // foreach($fResult as $element) { - // $ret = ''; - // eval('$ret = "' . $fConfig['display'] . '";'); - // $fData['filteroptions'][$element[$fConfig['key']]] = $ret; - // } - // } - // - // // field is flag field - // if($fName == $useModel->getIdentifier() . '_flag') { - // $flagConfig = $useModel->config->get('flag'); - // $fData['filteroptions'] = array(); - // foreach($flagConfig as $flagName => $flagValue) { - // $fData['filteroptions'][$flagValue] = app::getTranslate()->translate('DATAFIELD.' . $fName . '_' . $flagName); - // } - // } - // } + $value = []; + $elements = []; - // - // NOTE/EXPERIMENTAL: - // if $visibleFields contains one or more elements that are arrays - // (e.g. object-path-style fields) - // this may not work properly in some cases? - // - if(count($this->columnOrder) > 0) { - $visibleFields = array_values(array_unique(array_merge(array_intersect($this->columnOrder, $visibleFields), $visibleFields), SORT_REGULAR)); - } else { - $visibleFields = array_values(array_unique($visibleFields, SORT_REGULAR)); - } + foreach ($flags as $flagname => $flag) { + $value[$flagname] = !is_null($this->data) && $this->getMyModel()->isFlag($flag, $this->data->getData()); + $elements[] = [ + 'name' => $flagname, + 'display' => app::getTranslate()->translate('DATAFIELD.' . $field . '_' . $flagname), + 'value' => $flag, + ]; + } - $resultData = $this->resultData ?? $this->getMyModel()->search()->getResult(); + $fielddata = [ + 'field_name' => $this->getMyModel()->table . '_flag', + 'field_type' => 'multicheckbox', + 'field_datatype' => 'structure', + 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $field), + 'field_multiple' => true, + 'field_value' => $value, + 'field_elements' => $elements, + 'field_idfield' => 'name', + 'field_displayfield' => '{$element["display"]}', // todo: translate! + 'field_valuefield' => 'value', + ]; + + if ($this->readOnly || ($this->config->exists('readonly') && is_array($this->config->get('readonly')) && in_array($field, $this->config->get('readonly')))) { + $fielddata['field_readonly'] = true; + } - // - // Seek mode runtime ordering - // - if($this->crudSeekOverridePkeyOrder) { - // - // Stable usort based on main models' PKEY - // this is done in reverse, as we previously changed core ordering in ::makePagination - // - self::stable_usort($resultData, function($a, $b){ - // - // NOTE: we use the spaceship operator here, which outputs -1, 0 or 1 depending on value equality - // and we finally multiply it by -1 to re-gain the old/original PKEY ordering - // - return ($a[$this->getMyModel()->getPrimarykey()] <=> $b[$this->getMyModel()->getPrimarykey()]) - * - ($this->crudSeekOverridePkeyOrder === 'ASC' ? -1 : 1); - }); - } + $c = &$this->onCreateFormfield; + if ($c !== null && is_callable($c)) { + $c($fielddata); + } + $formField = new field($fielddata); - if(count($this->resultsetModifiers) > 0) { - foreach($this->resultsetModifiers as $modifier) { - $resultData = $modifier($resultData); - } - } + $c = &$this->onFormfieldCreated; + if ($c !== null && is_callable($c)) { + $c($formField); + } - // Send data to the response - if($this->rawMode) { - $this->getResponse()->setData('rows', $resultData); - } else { - $this->getResponse()->setData('rows', $this->makeFields($resultData, $visibleFields)); - } + $this->getForm()->addField($formField); - $this->getResponse()->setData('filterform', $filterForm ? $filterForm->output(true) : null); + continue; + } - $this->getResponse()->setData('topActions', $this->prepareActionsOutput($this->config->get("action>top") ?? [])); - $this->getResponse()->setData('bulkActions', $this->prepareActionsOutput($this->config->get("action>bulk") ?? [])); - $this->getResponse()->setData('elementActions', $this->prepareActionsOutput($this->config->get("action>element") ?? [])); - $this->getResponse()->setData('fieldActions', $this->prepareActionsOutput($fieldActions) ?? []); - $this->getResponse()->setData('visibleFields', $visibleFields); - $this->getResponse()->setData('availableFields', $availableFields); + $options = []; + if (($this->fieldsformConfig['readonly'] ?? false) && in_array($field, $this->fieldsformConfig['readonly'])) { + $options['field_readonly'] = true; + } + if (($this->fieldsformConfig['required'] ?? false) && in_array($field, $this->fieldsformConfig['required'])) { + $options['field_required'] = true; + } + if ($this->config->exists('readonly') && is_array($this->config->get('readonly')) && in_array($field, $this->config->get('readonly'))) { + $options['field_readonly'] = true; + } + if ($this->config->exists('required') && is_array($this->config->get('required')) && in_array($field, $this->config->get('required'))) { + $options['field_required'] = true; + } - // $this->getResponse()->setData('filters', $filters); - $this->getResponse()->setData('crud_filter_identifier', self::CRUD_FILTER_IDENTIFIER); - $this->getResponse()->setData('filters_used', $filterForm ? $filterForm->normalizeData($filterForm->getData()) : null); - // $this->getResponse()->setData('filters_unused', $filters); - $this->getResponse()->setData('enable_search_bar', $this->config->exists("visibleFilters>_search")); - $this->getResponse()->setData('modelinstance', $this->getMyModel()); + $this->getForm()->addField($this->makeField($field, $options))->setType('compact'); + } - // editable mode: - if($this->getRequest()->getData('crud_editable')) { - $form = $this->makeForm(null, false); - $this->getResponse()->setData('formconfig', $form->output(true)); + if ($addSubmitButton) { + $this->getForm()->addField( + (new field([ + 'field_name' => 'name', + 'field_title' => app::getTranslate()->translate('BUTTON.BTN_SAVE'), + 'field_description' => 'description', + 'field_id' => 'submit', + 'field_type' => 'submit', + 'field_value' => app::getTranslate()->translate('BUTTON.BTN_SAVE'), + ]))->setType('compact') + ); } - // - // Alternative pagination method: seek - // provide first and last id fetched - // - if($this->getConfig()->get('seek') === true) { - $rows = $this->getResponse()->getData('rows'); - $first = reset($rows); - $last = end($rows); - $this->getResponse()->addData([ - 'crud_pagination_first_id' => $first[$this->getMyModel()->getPrimarykey()], - 'crud_pagination_last_id' => $last[$this->getMyModel()->getPrimarykey()] - ]); - } - return; + $form = $this->getForm(); + + // pass the output config type to the form instance + $form->outputConfig = $this->outputFormConfig; + + return $form; } /** - * [stats description] - * @return void + * Loads one object from the CRUD generator's model if the primary key is defined. + * @param null|int|string $primarykey + * @return crud + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception */ - public function stats() { - $this->applyFilters(); + public function useEntry(null|int|string $primarykey = null): static + { + if (is_null($primarykey)) { + $this->getResponse()->setData('CRUD_FEEDBACK', 'ENTRY_CREATE'); + return $this; + } + $this->getResponse()->setData('CRUD_FEEDBACK', 'ENTRY_UPDATE'); + $this->data = new datacontainer($this->getMyModel()->load($primarykey)); - if($this->allowPagination) { - $this->makePagination(); - } + return $this; } /** - * stable usort function + * returns the current crud configuration + * + * @return config [description] */ - protected static function stable_usort(array &$array, $value_compare_func) + public function getConfig(): config { - $index = 0; - foreach ($array as &$item) { - $item = array($index++, $item); - } - $result = usort($array, function($a, $b) use($value_compare_func) { - $result = call_user_func($value_compare_func, $a[1], $b[1]); - return $result == 0 ? $a[0] - $b[0] : $result; - }); - foreach ($array as &$item) { - $item = $item[1]; - } - return $result; + return $this->config; } /** - * [protected description] - * @var array|null + * set config by identifier + * @param string $identifier [description] + * @return crud [description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception */ - protected $resultData = null; + public function setConfig(string $identifier = ''): crud + { + $this->config = $this->loadConfig($identifier); - /** - * [setResultData description] - * @param array $data [description] - */ - public function setResultData(array $data) { - $this->resultData = $data; + $this->customizedFields = $this->config->get('customized_fields') ?? []; + + if ($identifier !== '') { + $this->updateChildCrudConfigs(); + } + + return $this; } /** - * prepare action configs for output - * - * @param array $actions [description] - * @return array [description] + * [updateChildCrudConfigs description] + * @return void [type] [description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception */ - protected function prepareActionsOutput(array $actions) : array { - $handled = []; - foreach($actions as $key => $value) { - // we can't do this at the moment using our Vue App framework - // if(!array_key_exists('context', $value)) { - // $value['context'] = app::getRequest()->getData('context'); - // } - if(array_key_exists('_security', $value) && array_key_exists('group', $value['_security'])) { - if(!app::getAuth()->memberOf($value['_security']['group'])) { - continue; - } - } - if(array_key_exists('condition', $value)) { - eval($value['condition']); - if(!$condition) { - continue; + protected function updateChildCrudConfigs(): void + { + if ($this->config->exists('children_config')) { + $childrenConfig = $this->config->get('children_config'); + foreach ($childrenConfig as $childName => $childConfig) { + if (isset($this->childCruds[$childName])) { + if (isset($childConfig['crud'])) { + $this->childCruds[$childName]->setConfig($childConfig['crud']); + } + if (isset($childConfig['form'])) { + $this->childCruds[$childName]->useForm($childConfig['form']); + } + } } } - $value['display'] = app::getTranslate()->translate("BUTTON.BTN_" . $key); - - $handled[$key] = $value; - } - return $handled; } /** - * internal and temporary pagination switch (for exporting) - * @var bool + * russian function + * limits field output to visibleFields & optionally: "internalFields" in crud config + * @return void + * @throws ReflectionException + * @throws exception */ - protected $allowPagination = true; + public function limitFieldOutput(): void + { + $visibleFields = $this->getConfig()->get('visibleFields'); + $internalFields = $this->getConfig()->get('internalFields') ?? []; + $this->model->hideAllFields()->addField(implode(',', array_merge($visibleFields, $internalFields))); + foreach ($this->childCruds as $crud) { + $crud->limitFieldOutput(); + } + } /** - * [export description] - * @param bool $raw [enables raw export] - * @return void + * Enable/disable caching of the crud config + * @param bool $state [description] + * @return crud [description] */ - public function export(bool $raw = false) { - // disable limit and offset temporarily - $this->allowPagination = false; - $this->rawMode = $raw; - $this->listview(); - $this->rawMode = false; - $this->allowPagination = true; + public function setConfigCache(bool $state): crud + { + $this->useConfigCache = $state; + return $this; } /** - * defines raw, unformatted mode - * @var bool + * [setCustomizedFields description] + * @param array $fields [description] */ - protected $rawMode = false; + public function setCustomizedFields(array $fields): void + { + $this->customizedFields = $fields; + } /** - * imports a previously exported dataset - * - * @param array $data [description] - * @param bool $ignorePkeys [description] - * @return void + * I will add a modifier for a field. + * Modifiers are used to change the output of a field in the CRUD list. + * @param string $field + * @param callable $modifier + * @return crud + * @example addModifier('status_id', function($row) {return ''.$row['status_name'].''; }); */ - public function import(array $data, bool $ignorePkeys = true) { - foreach($data as $dataset) { - $this->getMyModel()->reset(); - if(count($errors = $this->getMyModel()->validate($dataset)->getErrors()) > 0) { - // erroneous dataset found - throw new exception('CRUD_IMPORT_INVALID_DATASET', exception::$ERRORLEVEL_ERROR, $errors); - } - } - - foreach($data as &$dataset) { - if(($dataset[$this->getMyModel()->getPrimarykey()] ?? false) && $ignorePkeys) { - unset($dataset[$this->getMyModel()->getPrimarykey()]); - } - // TODO: recurse? - $this->getMyModel()->entryMake($dataset)->entrySave(); - } - - $this->getResponse()->setData('import_data', $data); + public function addModifier(string $field, callable $modifier): crud + { + $this->modifiers[$field] = $modifier; + return $this; + } - return; + /** + * add a modifier for modifying the whole resultset + * @param callable $modifier [description] + * @return crud [description] + */ + public function addResultsetModifier(callable $modifier): crud + { + $this->resultsetModifiers[] = $modifier; + return $this; } /** - * Adds a top action - * @param array $action - * @return void + * configures column ordering for crud + * @param array $columns [description] + * @return crud */ - public function addTopaction(array $action) { - $this->addAction('top', $action); - return; + public function setColumnOrder(array $columns = []): crud + { + $this->columnOrder = $columns; + return $this; } /** - * Adds a bulk action - * @param array $action - * @return void + * I will add a ROW modifier + * which can change the output "" elements attributes + * @param callable $modifier [description] + * @return crud [description] + * @example addModifier('status_id', function($row) {return ''.$row['status_name'].''; }); */ - public function addBulkaction(array $action) { - $this->addAction('bulk', $action); - return; + public function addRowModifier(callable $modifier): crud + { + $this->rowModifiers[] = $modifier; + return $this; } /** - * Adds an element action - * @param array $action + * Returns the config for listview() * @return void + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception */ - public function addElementaction(array $action) { - $this->addAction('element', $action); - return; - } + public function listconfig(): void + { + $visibleFields = $this->config->get('visibleFields'); - /** - * Adds the important fields to the form instance of this crud editor - * - * @param string|null $primarykey [primary key of the entry to be used as value base or null] - * @param bool $addSubmitButton [whether the form should add a submit button field by default] - * @return \codename\core\ui\form [the form (also contained in this crud instance, accessible via ->getForm())] - */ - public function makeForm($primarykey = null, $addSubmitButton = true) : \codename\core\ui\form { - $this->useEntry($primarykey); + // Only append primarykey, if not added to visibleFields + if (!in_array($this->getMyModel()->getPrimaryKey(), $visibleFields)) { + $visibleFields[] = $this->getMyModel()->getPrimaryKey(); + } - // set request data only visible for this form - // usually, this is the complete request - // but it may only be a part of it. - $this->getForm()->setFormRequest($this->getFormNormalizationData()); + $formattedFields = []; + + // + // Format foreign key values as defined by the model + // + if (!is_null($this->getMyModel()->config->get('foreign'))) { + $foreignKeys = $this->getMyModel()->config->get('foreign'); - if($this->config->exists('tag')) { - $this->getForm()->config['form_tag'] = $this->config->get('tag'); + $formattedFields = array_reduce(array_keys($foreignKeys), function ($carry, $key) { + // foreign keys use a formatted output field AND a data key + $carry[$key] = $key . '_FORMATTED'; + return $carry; + }, $formattedFields); } - // use "field", if defined in crud config - if($this->config->exists('field') && count($this->fields) === 0) { - $this->fields = $this->config->get('field'); + // + // also include "modifier" fields as _FORMATTED ones. + // + $formattedFields = array_merge( + $formattedFields, + array_reduce(array_keys($this->modifiers), function ($carry, $key) { + // use a modifier key as final field + $carry[$key] = $key; + return $carry; + }, []) + ); + + // + // Fields that are available as raw data AND as a _FORMATTED one + // + $this->getResponse()->setData('formattedFields', $formattedFields); + + // + // Enable custom selection of displayed fields (columns) + // + $this->getResponse()->setData('enable_displayfieldselection', ($this->config->exists('displayFieldSelection') ? $this->config->get('displayFieldSelection') : false)); + + if ($this->config->exists('availableFields')) { + $availableFields = $this->config->get('availableFields'); + } else { + // enable ALL fields of the model to be displayed + $availableFields = $this->getMyModel()->config->get('field'); } - if(count($this->fields) == 0 && count($this->getForm()->getFieldsets()) == 0) { - $this->fields = $this->getMyModel()->config->get('field'); + // add formatted fields to availableFields + $availableFields = array_merge($availableFields, array_keys($formattedFields)); + + // remove all disabled fields + if ($this->config->exists('disabled')) { + $availableFields = array_diff($availableFields, $this->config->get('disabled')); } - // Be sure to show the primary key in the form - array_push($this->fields, $this->getMyModel()->getPrimarykey()); - $this->fields = array_unique($this->fields); + // merge and kill duplicates + $availableFields = array_values(array_unique($availableFields)); - foreach($this->fields as $field) { - if($this->config->exists('disabled') && is_array($this->config->get('disabled')) && in_array($field, $this->config->get('disabled'))) { - continue; + $displayFields = []; + + // display fields are either the visibleFields (defined in config) or submitted + // in the latter case; we have to check for legitimacy first. + if ($this->getRequest()->isDefined('display_selectedfields') && $this->getResponse()->getData('enable_displayfieldselection')) { + $selectedFields = $this->getRequest()->getData('display_selectedfields'); + if (is_array($selectedFields)) { + // + // NOTE/CHANGED 2019-06-14: we have to include some more fields + // + $avFields = array_unique(array_merge($visibleFields, $availableFields)); + foreach ($selectedFields as $displayField) { + if (in_array($displayField, $avFields)) { + $displayFields[] = $displayField; + } + } } + } + + if (count($displayFields) > 0) { + $visibleFields = $displayFields; + } else { + // add all modifier fields by default + // if no field selection provided + $visibleFields = array_merge($visibleFields, array_keys($this->modifiers)); + } + + if (!in_array($this->getMyModel()->getPrimaryKey(), $visibleFields)) { + $visibleFields[] = $this->getMyModel()->getPrimaryKey(); + } - if(in_array($field, array($this->getMyModel()->table . "_modified", $this->getMyModel()->table . "_created"))) { + // + // Provide some labels for frontend display + // + $fieldLabels = []; + foreach (array_merge($availableFields, $visibleFields, $formattedFields) as $field) { + if (!is_string($field)) { continue; } - if(!in_array($field, $this->getMyModel()->config->get('field'))) { - throw new \codename\core\exception(self::EXCEPTION_MAKEFORM_FIELDNOTFOUNDINMODEL, \codename\core\exception::$ERRORLEVEL_ERROR, $field); + $fieldLabels[$field] = app::getTranslate()->translate('DATAFIELD.' . $field); + } + foreach ($availableFields as $field) { + if ($fieldLabels[$field] ?? false) { + $fieldLabels[$field] = app::getTranslate()->translate('DATAFIELD.' . $field); } - - // exclude child model fields that have an active children config for this crud - if($this->config->exists('children') && $this->getMyModel()->config->exists('children')) { - // if field exists in a child config field reference - $found = false; - foreach($this->config->get('children') as $childField) { - if(($childConfig = $this->getMyModel()->config->get('children>'.$childField)) !== null) { - if($childConfig['type'] === 'foreign' && $childConfig['field'] == $field) { - $found = true; - break; - } else if($childField === $field && $childConfig['type'] === 'collection') { - // $found = true; - break; - } - } - } - if($found) { - continue; - } + } + $this->getResponse()->setData('labels', $fieldLabels); + if ($this->getConfig()->exists('export>_security>group')) { + if ($enableExport = app::getAuth()->memberOf($this->getConfig()->get('export>_security>group'))) { + $this->getResponse()->setData('export_types', $this->getConfig()->get('export>allowedTypes')); } + $this->getResponse()->setData('enable_export', $enableExport); + } else { + $this->getResponse()->setData('enable_export', false); + } - if($field == $this->getMyModel()->table . '_flag') { - $flags = $this->getMyModel()->config->get('flag'); - if(!is_array($flags)) { - continue; - } + if ($this->getConfig()->exists('import>_security>group')) { + if ($enableImport = app::getAuth()->memberOf($this->getConfig()->get('import>_security>group'))) { + // $this->getResponse()->setData('export_types', $this->getConfig()->get('export>allowedTypes')); + } + $this->getResponse()->setData('enable_import', $enableImport); + } else { + $this->getResponse()->setData('enable_import', false); + } - $value = []; - $elements = []; + $fieldActions = $this->config->get("action>field") ?? []; + $filters = $this->config->get('visibleFilters', []); + // merge-in provided filters + $filters = array_merge($filters, $this->providedFilters); - foreach($flags as $flagname => $flag) { - $value[$flagname] = !is_null($this->data) ? $this->getMyModel()->isFlag($flag, $this->data->getData()) : false; - $elements[] = [ - 'name' => $flagname, - 'display' => app::getTranslate()->translate('DATAFIELD.' . $field . '_' . $flagname), - 'value' => $flag - ]; - } + // + // build a form from filters + // + $filterForm = null; - $fielddata = array ( - 'field_name' => $this->getMyModel()->table . '_flag', - 'field_type' => 'multicheckbox', - 'field_datatype' => 'structure', - 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $field), - 'field_multiple' => true, - 'field_value' => $value, - 'field_elements' => $elements, - 'field_idfield' => 'name', - 'field_displayfield' => '{$element["display"]}', // todo: translate! - 'field_valuefield' => 'value' - ); + if (count($filters) > 0) { + $filterForm = new form([ + 'form_id' => 'filterform', + 'form_method' => 'post', + 'form_action' => '', + ]); - if($this->readOnly || ($this->config->exists('readonly') && is_array($this->config->get('readonly')) && in_array($field, $this->config->get('readonly')))) { - $fielddata['field_readonly'] = true; - } + $filterForm->setFormRequest($this->getRequest()->getData(self::CRUD_FILTER_IDENTIFIER) ?? []); - $c = &$this->onCreateFormfield; - if($c !== null && is_callable($c)) { - $c($fielddata); - } + foreach ($filters as $filterSpecifier => $filterConfig) { + $specifier = explode('.', $filterSpecifier); + $useModel = $this->getMyModel(); - $formField = new \codename\core\ui\field($fielddata); + $fName = $specifier[count($specifier) - 1]; - $c = &$this->onFormfieldCreated; - if($c !== null && is_callable($c)) { - $c($formField); + if (count($specifier) == 2) { + // we have a model/table reference + $useModel = $this->getModel($specifier[0]); } - $this->getForm()->addField($formField); - - continue; - } + // field is a foreign key + if (!($filterConfig['wildcard'] ?? false) && in_array($fName, $useModel->config->get('field'))) { + $field = $this->makeFieldForeign($useModel, $fName, $filterConfig); // options? + } elseif ($filterConfig['config']['field_config'] ?? false) { + $fieldData = array_merge( + [ + 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $fName), + 'field_name' => $filterSpecifier, + 'field_type' => 'input', + ], + $filterConfig['config']['field_config'] + ); + $field = new field($fieldData); + } else { + // wildcard, no normalization needed + $field = new field([ + 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $fName), + 'field_name' => $filterSpecifier, + 'field_type' => 'input', + ]); + } - $options = []; - if (($this->fieldsformConfig['readonly'] ?? false) && in_array($field, $this->fieldsformConfig['readonly'])) { - $options['field_readonly'] = true; - } - if (($this->fieldsformConfig['required'] ?? false) && in_array($field, $this->fieldsformConfig['required'])) { - $options['field_required'] = true; + $filterForm->addField($field); } - if($this->config->exists('readonly') && is_array($this->config->get('readonly')) && in_array($field, $this->config->get('readonly'))) { - $options['field_readonly'] = true; - } - if($this->config->exists('required') && is_array($this->config->get('required')) && in_array($field, $this->config->get('required'))) { - $options['field_required'] = true; - } - - $this->getForm()->addField($this->makeField($field, $options))->setType('compact'); } - if($addSubmitButton) { - $this->getForm()->addField((new field(array( - 'field_name' => 'name', - 'field_title' => app::getTranslate()->translate('BUTTON.BTN_SAVE'), - 'field_description' => 'description', - 'field_id' => 'submit', - 'field_type' => 'submit', - 'field_value' => app::getTranslate()->translate('BUTTON.BTN_SAVE') - )))->setType('compact')); + if (count($this->columnOrder) > 0) { + $visibleFields = array_values(array_unique(array_merge(array_intersect($this->columnOrder, $visibleFields), $visibleFields), SORT_REGULAR)); + } else { + $visibleFields = array_values(array_unique($visibleFields, SORT_REGULAR)); } + $this->getResponse()->setData('filterform', $filterForm?->output(true)); - $form = $this->getForm(); - - // pass the output config type to the form instance - $form->outputConfig = $this->outputFormConfig; - - return $form; + $this->getResponse()->setData('topActions', $this->prepareActionsOutput($this->config->get("action>top") ?? [])); + $this->getResponse()->setData('bulkActions', $this->prepareActionsOutput($this->config->get("action>bulk") ?? [])); + $this->getResponse()->setData('elementActions', $this->prepareActionsOutput($this->config->get("action>element") ?? [])); + $this->getResponse()->setData('fieldActions', $this->prepareActionsOutput($fieldActions) ?? []); + $this->getResponse()->setData('visibleFields', $visibleFields); + $this->getResponse()->setData('availableFields', $availableFields); + $this->getResponse()->setData('crud_filter_identifier', self::CRUD_FILTER_IDENTIFIER); + $this->getResponse()->setData('filters_used', $filterForm?->normalizeData($filterForm->getData())); + $this->getResponse()->setData('enable_search_bar', $this->config->exists("visibleFilters>_search")); + $this->getResponse()->setData('modelinstance', $this->getMyModel()); } /** - * This event will be fired whenever a method of this CRUD instance generates a form instance. - *
Use this event to alter the current form of the CRUD instance (e.g. for asking for more fields) - * @var event + * function for making fields, independent of the current crud model + * @param model $model [description] + * @param string $field [description] + * @param array $options [description] + * @return field [description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception */ - public $eventCrudFormInit; + public function makeFieldForeign(model $model, string $field, array $options = []): field + { + // load model config for simplicity + $modelconfig = $model->config->get(); + + // Error if field not in models + if (!in_array($field, $model->getFields())) { + throw new exception(self::EXCEPTION_MAKEFIELD_FIELDNOTFOUNDINMODEL, exception::$ERRORLEVEL_ERROR, $field); + } + + // Create a basic formfield array + $fielddata = [ + 'field_id' => $field, + 'field_name' => $field, + 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $field), + 'field_description' => app::getTranslate()->translate('DATAFIELD.' . $field . '_DESCRIPTION'), + 'field_type' => 'input', + 'field_required' => $options['field_required'] ?? false, + 'field_placeholder' => app::getTranslate()->translate('DATAFIELD.' . $field), + 'field_multiple' => false, + 'field_readonly' => $options['field_readonly'] ?? false, + ]; - /** - * This event is fired before the validation starts. - * @example Imagine cases where you don't want a user to input data but you must - *
add it to the entry, because the missing fields would violate the model's - *
constraints. Here you can do anything you want with the entry array. - * @var event - */ - public $eventCrudBeforeValidation; + // Get the displaytype of this field + if (array_key_exists('datatype', $modelconfig) && array_key_exists($field, $modelconfig['datatype'])) { + $fielddata['field_type'] = $this->getDisplaytype($modelconfig['datatype'][$field]); + $fielddata['field_datatype'] = $modelconfig['datatype'][$field]; + } - /** - * This event is fired after validation has been successful. - * @var event - */ - public $eventCrudAfterValidation; + if ($fielddata['field_type'] == 'yesno') { + $fielddata['field_type'] = 'select'; + $fielddata['field_displayfield'] = '{$element[\'field_name\']}'; + $fielddata['field_valuefield'] = 'field_value'; - /** - * This event is fired after validation has been successful. - * We might run additional validators here. - * output must be either null, empty array or errors found in additional validators - * @var event - */ - public $eventCrudValidation; + // NOTE: Datatype for this kind of pseudo-boolean field must be null or so + // because the boolean validator really needs a bool. + $fielddata['field_datatype'] = null; + $fielddata['field_elements'] = [ + [ + 'field_value' => true, + 'field_name' => 'Ja', + ], + [ + 'field_value' => false, + 'field_name' => 'Nein', + ], + ]; + } - /** - * This event is fired whenever the CRUD generator wants to save a validated entry (or updates) - *
to a model. It is given the $data and must return the $data. - * @example Imagine you want to manipulate entries on a model when saving the entry - *
from the CRUD generator. This is version will happen after the validation. - * @var event - */ - public $eventCrudBeforeSave; + // Modify field to be a reference dropdown + if (array_key_exists('foreign', $modelconfig) && array_key_exists($field, $modelconfig['foreign'])) { + if (!app::getValidator('structure_config_modelreference')->reset()->isValid($modelconfig['foreign'][$field])) { + throw new exception(self::EXCEPTION_MAKEFIELD_INVALIDREFERENCEOBJECT, exception::$ERRORLEVEL_ERROR, $modelconfig['foreign'][$field]); + } - /** - * This event is fired whenever the CRUD generator successfully completed an operation - *
to a model. It is given the $data. - * @var event - */ - public $eventCrudSuccess; + $foreign = $modelconfig['foreign'][$field]; - /** - * Returns a form HTML code for creating an object. After validating the data in the form class AND validating the data in the model class, we will try to store the data in the model. - */ - public function create() { - $this->getResponse()->setData('context', 'crud'); + $elements = $this->getModel($foreign['model'], $foreign['app'] ?? app::getApp()); - $form = $this->makeForm(); + // + // skip the basic model setup if we're using the remote api interface anyway. + // + if (!($elements instanceof exposesRemoteApiInterface) || !isset($foreign['remote_source'])) { + if (array_key_exists('order', $foreign) && is_array($foreign['order'])) { + foreach ($foreign['order'] as $order) { + if (!app::getValidator('structure_config_modelorder')->reset()->isValid($order)) { + throw new exception(self::EXCEPTION_MAKEFIELD_INVALIDORDEROBJECT, exception::$ERRORLEVEL_ERROR, $order); + } + $elements->addOrder($order['field'], $order['direction']); + } + } - // OLD: $hookval = app::getHook()->fire(\codename\core\hook::EVENT_CRUD_CREATE_FORM_INIT, $form); - // Fire the form init event - $hookval = $this->eventCrudFormInit->invokeWithResult($this, $form); - if(is_object($hookval) && $hookval instanceof \codename\core\ui\form) { - $form = $hookval; - } + if (array_key_exists('filter', $foreign) && is_array($foreign['filter'])) { + foreach ($foreign['filter'] as $filter) { + if (!app::getValidator('structure_config_modelfilter')->reset()->isValid($filter)) { + throw new exception(self::EXCEPTION_MAKEFIELD_INVALIDFILTEROBJECT, exception::$ERRORLEVEL_ERROR, $filter); + } + if ($filter['field'] == $elements->getIdentifier() . '_flag') { + if ($filter['operator'] == '=') { + $elements->withFlag($elements->config->get('flag>' . $filter['value'])); + } elseif ($filter['operator'] == '!=') { + $elements->withoutFlag($elements->config->get('flag>' . $filter['value'])); + } else { + throw new exception(self::EXCEPTION_MAKEFIELD_FILTER_FLAG_INVALIDOPERATOR, exception::$ERRORLEVEL_ERROR, $filter); + } + } else { + $elements->addFilter($filter['field'], $filter['value'], $filter['operator']); + } + } + } + } - if(!$form->isSent()) { - $this->getResponse()->setData('form', $form->output($this->outputFormConfig)); - return; - } + $fielddata['field_type'] = 'select'; + $fielddata['field_displayfield'] = $foreign['display']; + $fielddata['field_valuefield'] = $foreign['key']; - $data = $this->getMyModel()->normalizeData( $this->getFormNormalizationData() ); + if ($elements instanceof exposesRemoteApiInterface && isset($foreign['remote_source'])) { + $apiEndpoint = $elements->getExposedApiEndpoint(); + $fielddata['field_remote_source'] = $apiEndpoint; - // OLD: $newData = app::getHook()->fire(\codename\core\hook::EVENT_CRUD_CREATE_BEFORE_VALIDATION, $data); - // Fire the before validation event - $newData = $this->eventCrudBeforeValidation->invokeWithResult($this, $data); - if(is_array($newData)) { - $data = $newData; - } + $remoteSource = $foreign['remote_source']; - // form validation before model validation - if(!$form->isValid()) { - $this->getResponse()->setStatus(\codename\core\response::STATUS_INTERNAL_ERROR); - $this->getResponse()->setData('errors', $form->getErrorstack()->getErrors()); - $this->getResponse()->setData('view', 'validation_error'); - return; - } + $filterKeys = []; + foreach ($remoteSource['filter_key'] as $filterKey => $filterData) { + if (is_array($filterData)) { + foreach ($filterData as $filterDataData) { + $filterKeys[$filterKey][$filterDataData] = true; + } + } else { + $filterKeys[$filterData] = true; + } + } - if(!$this->getMyModel()->isValid($data)) { - $this->getResponse()->setStatus(\codename\core\response::STATUS_INTERNAL_ERROR); - $this->getResponse()->setData('errors', $this->getMyModel()->getErrors()); - $this->getResponse()->setData('view', 'save_error'); - return; - } + $fielddata['field_remote_source_filter_key'] = $filterKeys; - // Fire hook after successful validation - // OLD app::getHook()->fire(\codename\core\hook::EVENT_CRUD_CREATE_AFTER_VALIDATION, $data); - $this->eventCrudAfterValidation->invoke($this, $data); + // + // Explicit Filter Key + // for retrieving an already set, unique and strictly defined value + // + if ($remoteSource['explicit_filter_key'] ?? false) { + $fielddata['field_remote_source_explicit_filter_key'] = $remoteSource['explicit_filter_key']; + } - // Fire hook for additional validators - // OLD: $errors = app::getHook()->fire(\codename\core\hook::EVENT_CRUD_CREATE_VALIDATION, $data); - $errorResults = $this->eventCrudValidation->invokeWithAllResults($this, $data); + $fielddata['field_remote_source_parameter'] = $remoteSource['parameters'] ?? []; + $fielddata['field_remote_source_display_key'] = $remoteSource['display_key'] ?? null; + $fielddata['field_remote_source_links'] = $foreign['remote_source']['links'] ?? []; + $fielddata['field_valuefield'] = $foreign['key']; + $fielddata['field_displayfield'] = $foreign['key']; + } elseif (!in_array($field, $this->customizedFields)) { + $fielddata['field_elements'] = $elements->search()->getResult(); + } + + // + // by default, we allow multiselect + // + $multiple = true; + if (array_key_exists('field_multiple', $options)) { + $multiple = $options['field_multiple']; + } elseif (array_key_exists('multiple', $options)) { + $multiple = $options['multiple']; + } + + if ($multiple) { + $fielddata['field_datatype'] = 'structure'; + $fielddata['field_multiple'] = $multiple; + } - $errors = array(); - foreach($errorResults as $errorCollection) { - if(count($errorCollection) > 0) { - $errors = array_merge($errors, $errorCollection); - } + if ($elementDatatype = $modelconfig['datatype'][$field] ?? false) { + // + // if multiselect, provide element datatype for correct conversions + // + if ($multiple) { + $fielddata['field_element_datatype'] = $elementDatatype; + } else { + $fielddata['field_datatype'] = $elementDatatype; + } + } } - if(count($errors) > 0) { - $this->getResponse()->setStatus(\codename\core\response::STATUS_INTERNAL_ERROR); - $this->getResponse()->setData('errors', $errors); - $this->getResponse()->setData('view', 'save_error'); - return; + $c = &$this->onCreateFormfield; + if ($c !== null && is_callable($c)) { + $c($fielddata); } - // OLD: app::getHook()->fire(\codename\core\hook::EVENT_CRUD_CREATE_BEFORE_SAVE, $data); - $this->eventCrudBeforeSave->invoke($this, $data); + $field = new field($fielddata); + $field->setType('compact'); - $this->getMyModel()->saveWithChildren($data); + $c = &$this->onFormfieldCreated; + if ($c !== null && is_callable($c)) { + $c($field); + } - // OLD: app::getHook()->fire(\codename\core\hook::EVENT_CRUD_CREATE_SUCCESS, $data); - // eventCrudBeforeSave MUST NOT modify data, due to crud mechanics. Data might be modified in eventCrudBeforeValidation or so - $this->eventCrudSuccess->invoke($this, $data); + // Add the field to the form + return $field; + } - $this->getResponse()->setData($this->getMyModel()->getPrimarykey(), $this->getMyModel()->lastInsertId()); + /** + * prepare action configs for output + * + * @param array $actions [description] + * @return array [description] + * @throws ReflectionException + * @throws exception + */ + protected function prepareActionsOutput(array $actions): array + { + $handled = []; + foreach ($actions as $key => $value) { + if (array_key_exists('_security', $value) && array_key_exists('group', $value['_security'])) { + if (!app::getAuth()->memberOf($value['_security']['group'])) { + continue; + } + } + if (array_key_exists('condition', $value)) { + $condition = null; + eval($value['condition']); + if (!$condition) { + continue; + } + } + $value['display'] = app::getTranslate()->translate("BUTTON.BTN_" . $key); - $this->getResponse()->setData('view', 'crud_success'); + $handled[$key] = $value; + } + return $handled; } /** - * @todo DOCUMENTATION + * [stats description] + * @return void + * @throws ReflectionException + * @throws exception */ - public function bulkDelete() { - if(!$this->getRequest()->isDefined($this->getMyModel()->getPrimarykey())) { - return; + public function stats(): void + { + $this->applyFilters(); + + if ($this->allowPagination) { + $this->makePagination(); } } /** - * [bulkEdit description] + * Will apply defaultFilter properties to the model instance of this CRUD generator * @return void + * @throws ReflectionException + * @throws exception */ - public function bulkEdit() { - if($this->getRequest()->isDefined('data')) { - $data = $this->getRequest()->getData('data'); - - // - // Validate - // - foreach($data as $entry) { - - // get full entry with modified delta - if($entry[$this->getMyModel()->getPrimarykey()] ?? false) { - $currentEntry = $this->getMyModel()->load($entry[$this->getMyModel()->getPrimarykey()]); - } else { - $currentEntry = []; - } - $currentEntry = array_replace_recursive($currentEntry, $entry); - - // TODO: validate using bulk form? - - if(!$this->getMyModel()->isValid($currentEntry)) { - $this->getResponse()->setStatus(\codename\core\response::STATUS_INTERNAL_ERROR); - $this->getResponse()->setData('errors', $this->getMyModel()->getErrors()); - $this->getResponse()->setData('view', 'save_error'); - return; - } + protected function applyFilters(): void + { + if (!$this->getRequest()->isDefined(self::CRUD_FILTER_IDENTIFIER)) { + return; + } + $filters = $this->getRequest()->getData(self::CRUD_FILTER_IDENTIFIER); + if (!is_array($filters)) { + return; } - // - // Save - // - $transaction = new \codename\core\transaction('crud_bulk_edit', [ $this->getMyModel() ]); - $transaction->start(); - - $pkeyValues = []; - - foreach($data as $entry) { - // - // TODO: how to handle delta edits on nested models? - // - $this->getMyModel()->saveWithChildren($entry); - - if($pkeyValue = $entry[$this->getMyModel()->getPrimarykey()] ?? false) { - $pkeyValues[] = $pkeyValue; - } else { - $pkeyValues[] = $this->getMyModel()->lastInsertId(); - } + if (array_key_exists('search', $filters) && $filters['search'] != '') { + if ($this->config->exists("visibleFilters>_search") && is_array($this->config->get("visibleFilters>_search"))) { + $filterCollection = []; + foreach ($this->config->get("visibleFilters>_search>fields") as $field) { + $filterCollection[] = ['field' => $field, 'value' => $this->getFilterstring($filters['search'], $field, true), 'operator' => 'LIKE']; + } + $this->getMyModel()->addDefaultFilterCollection($filterCollection, 'OR'); + } + // Do NOT return; as we may use other filters, too. + // Why tho? + // Return; } - $transaction->end(); + foreach ($filters as $key => $value) { + // exclude a search key here, as we're not returning after a wildcard search anymore + if ($key === 'search') { + continue; + } - $this->getResponse()->setData($this->getMyModel()->getPrimarykey(), $pkeyValues); + if ($providedFilter = $this->providedFilters[$key] ?? false) { + $providedFilter['callback']($this, $value); + continue; + } - } else { - throw new exception('CRUD_BULK_EDIT_DATA_UNDEFINED', exception::$ERRORLEVEL_ERROR); - } + if ($key == $this->getMyModel()->getIdentifier() . '_flag') { + if (is_array($value)) { + foreach ($value as $flagval) { + $this->getMyModel()->withDefaultFlag($this->getFilterstring($flagval, $key)); + } + } else { + $this->getMyModel()->withDefaultFlag($this->getFilterstring($value, $key)); + } + } elseif (is_array($value) && $this->model->config->exists("datatype>" . $key) && in_array($this->model->config->get("datatype>" . $key), ['text_timestamp', 'text_date'])) { + $this->getMyModel()->addDefaultFilter($key, $this->getFilterstring($value[0], $key), '>='); + $this->getMyModel()->addDefaultFilter($key, $this->getFilterstring($value[1], $key), '<='); + } else { + $wildcard = $this->config->exists("visibleFilters>" . $key . ">wildcard") && ($this->config->get("visibleFilters>" . $key . ">wildcard") == true); + $operator = $this->config->exists("visibleFilters>" . $key . ">operator") ? $this->config->get("visibleFilters>" . $key . ">operator") : $this->getDefaultoperator($key, $wildcard); + $this->getMyModel()->addDefaultFilter($key, $this->getFilterstring($value, $key, $wildcard), $operator); + } + } } /** - * returns the request data - * that is used for normalization - * in form-related functions - * @return array + * Will return the filterable string used for the given field's datatype + * @param mixed $value [description] + * @param string $field [description] + * @param bool $wildcard [description] + * @return mixed [description] + * @throws ReflectionException + * @throws exception */ - protected function getFormNormalizationData() : array { - if($this->formNormalizationData == null) { - $this->setFormNormalizationData($this->getRequest()->getData()); - } - return $this->formNormalizationData; + protected function getFilterstring(mixed $value, string $field, bool $wildcard = false): mixed + { + if (is_array($value)) { + return $value; + } + return match ($this->getMyModel()->getFieldtype(new modelfield($field))) { + 'number_natural' => $value, + default => $wildcard ? '%' . $value . '%' : $value, + }; } /** - * [protected description] - * @var array - */ - protected $formNormalizationData = null; - - /** - * sets the underlying data used during normalization - * in the normal use case, this is the pure request data - * @param array $data [description] + * Returns the default CRUD filter for the given filters + * @param string $field [description] + * @param bool $wildcard [description] + * @return string [description] + * @throws ReflectionException + * @throws exception */ - public function setFormNormalizationData(array $data) { - $this->formNormalizationData = $data; + protected function getDefaultoperator(string $field, bool $wildcard = false): string + { + return match ($this->getMyModel()->getFieldtype(new modelfield($field))) { + 'number_natural' => '=', + default => $wildcard ? 'LIKE' : '=', + }; } /** - * Returnes the form HTML code for editing an existing entry. Will make sure the given data is compliant to the form's and model's configuration - * @param string|int $primarykey + * Sends the pagination data to the response + * @return void + * @throws ReflectionException + * @throws exception */ - public function edit($primarykey) { - $form = $this->makeForm($primarykey); - - // OLD: $hookval = app::getHook()->fire(\codename\core\hook::EVENT_CRUD_EDIT_FORM_INIT, $form); - // Fire the form init event - $hookval = $this->eventCrudFormInit->invokeWithResult($this, $form); + protected function makePagination(): void + { + // + // small HACK: + // temporarily add a count field, + // perform the query using the given configuration + // and remove it afterward. + // + if ($this->getConfig()->get('seek') || $this->getRequest()->getData('crud_stats_async')) { + $count = null; + } else { + $start = microtime(true); + $count = $this->getMyModel()->getCount(); + $end = microtime(true); - if(is_object($hookval) && $hookval instanceof \codename\core\ui\form) { - $form = $hookval; + // DEBUG! + $this->getResponse()->setData('_count_time', ($end - $start)); } - if($this->config->exists('action>crud_edit')) { - $this->getResponse()->setData('editActions', $this->config->get('action>crud_edit')); + // default value, if none of the below works: + $page = 1; + if ($this->getRequest()->isDefined('crud_pagination_page')) { + // explicit page request + $page = (int)$this->getRequest()->getData('crud_pagination_page'); + } elseif ($this->getRequest()->isDefined('crud_pagination_page_prev')) { + // fallback to previous page value, if page hasn't been submitted + $page = (int)$this->getRequest()->getData('crud_pagination_page_prev'); } - if(!$form->isSent()) { - $this->getResponse()->setData('form', $form->output($this->outputFormConfig)); - return; + if ($this->getRequest()->isDefined('crud_pagination_limit')) { + $limit = (int)$this->getRequest()->getData('crud_pagination_limit'); + } else { + $limit = $this->config->get("pagination>limit", 10); } - // we can use $form->getData() here, but then we're receiving a lot more data (e.g. non-input or disabled fields!) - $data = $this->getMyModel()->normalizeData( $this->getFormNormalizationData() ); - - // DEBUG: \codename\core\app::getResponse()->setData('crud_debug_'.$this->model->getIdentifier().'_data_incoming', $data); - - // OLD: $newData = app::getHook()->fire(\codename\core\hook::EVENT_CRUD_EDIT_BEFORE_VALIDATION, $data); - $newData = $this->eventCrudBeforeValidation->invokeWithResult($this, $data); - if(is_array($newData)) { - $data = $newData; + if ($this->getConfig()->get('seek') || $this->getRequest()->getData('crud_stats_async')) { + $pages = null; + } else { + $pages = ($limit == 0 || $count == 0) ? 1 : ceil($count / $limit); } - // form validation before model validation - if(!$form->isValid()) { - $this->getResponse()->setStatus(\codename\core\response::STATUS_INTERNAL_ERROR); - $this->getResponse()->setData('errors', $form->getErrorstack()->getErrors()); - $this->getResponse()->setData('view', 'validation_error'); - return; + // when not in seek mode (normal mode), limit last page to max. page available + if (!$this->getConfig()->get('seek') && !$this->getRequest()->getData('crud_stats_async')) { + // pagination limit change with present page param, that is out of range: + if ($page > $pages) { + $page = $pages; + } } - // DEBUG: \codename\core\app::getResponse()->setData('crud_debug_'.$this->model->getIdentifier().'_data_new', $newData); - - $this->getMyModel()->entryLoad($primarykey); - // DEBUG: \codename\core\app::getResponse()->setData('crud_debug_'.$this->model->getIdentifier().'_entry_loaded', $this->getMyModel()->getData()); - - $this->getMyModel()->entryUpdate($data); - // DEBUG: \codename\core\app::getResponse()->setData('crud_debug_'.$this->model->getIdentifier().'_entry_updated', $this->getMyModel()->getData()); - - - if(count($errors = $this->getMyModel()->entryValidate()) > 0) { - $this->getResponse()->setStatus(\codename\core\response::STATUS_INTERNAL_ERROR); - $this->getResponse()->setData('errors', $errors); - $this->getResponse()->setData('view', 'save_error'); - return; - } + if ($this->getConfig()->get('seek') === true) { + // + // Alternative pagination method: Seeking! + // + $firstId = $this->getRequest()->getData('crud_pagination_first_id'); + $lastId = $this->getRequest()->getData('crud_pagination_last_id'); + $seekMode = $this->getRequest()->getData('crud_pagination_seek') ?? 0; + + $ordering = 'ASC'; // is this really our default? + if ($this->config->get("order")) { + foreach ($this->config->get("order") as $order) { + if ($order['field'] === $this->getMyModel()->getPrimaryKey()) { + $ordering = $order['direction']; + } + } + } - // Fire hook after successful validation - // OLD: app::getHook()->fire(\codename\core\hook::EVENT_CRUD_EDIT_AFTER_VALIDATION, $data); - $this->eventCrudAfterValidation->invoke($this, $data); + // stable position + if ($firstId && $seekMode == 0) { + $operator = $ordering === 'ASC' ? '>=' : '<='; + $this->getMyModel()->addFilter($this->getMyModel()->getPrimaryKey(), $firstId, $operator); + } - // Fire hook for additional validators - // OLD: $errors = app::getHook()->fire(\codename\core\hook::EVENT_CRUD_EDIT_VALIDATION, $data); - $errorResults = $this->eventCrudValidation->invokeWithAllResults($this, $data); + // we're moving backwards + if ($firstId && $seekMode < 0) { + $operator = $ordering === 'ASC' ? '<' : '>'; + $this->getMyModel()->addFilter($this->getMyModel()->getPrimaryKey(), $firstId, $operator); + $this->crudSeekOverridePkeyOrder = $ordering === 'ASC' ? 'DESC' : 'ASC'; // enable overriding the other ordering... + } - $errors = array(); - foreach($errorResults as $errorCollection) { - if(count($errorCollection) > 0) { - $errors = array_merge($errors, $errorCollection); - } - } + // we're moving forward + if ($lastId && $seekMode > 0) { + $operator = $ordering === 'ASC' ? '>' : '<'; + $this->getMyModel()->addFilter($this->getMyModel()->getPrimaryKey(), $lastId, $operator); + } - if(count($errors) > 0) { - $this->getResponse()->setStatus(\codename\core\response::STATUS_INTERNAL_ERROR); - $this->getResponse()->setData('errors', $errors); - $this->getResponse()->setData('view', 'save_error'); - return; + $this->getMyModel()->setLimit($limit); + } elseif ($pages > 1 || $this->getRequest()->getData('crud_stats_async')) { + $this->getMyModel()->setLimit($limit)->setOffset(($page - 1) * $limit); } - // OLD: $newData = app::getHook()->fire(\codename\core\hook::EVENT_CRUD_EDIT_BEFORE_SAVE, $data); - // eventCrudBeforeSave MUST NOT modify data, due to crud mechanics. Data might be modified in eventCrudBeforeValidation or so - $this->eventCrudBeforeSave->invoke($this, $data); - - $this->getMyModel()->entryUpdate($data); - $this->getMyModel()->entrySave(); - - // OLD:: app::getHook()->fire(\codename\core\hook::EVENT_CRUD_EDIT_SUCCESS, $data); - $newData = $this->eventCrudSuccess->invokeWithResult($this, $data); - - $this->getResponse()->setData('view', 'crud_success'); + // Response + $this->getResponse()->addData( + [ + 'crud_pagination_seek_enabled' => $this->getConfig()->get('seek') === true, + 'crud_pagination_count' => $count, + 'crud_pagination_page' => $page, + 'crud_pagination_pages' => $pages, + 'crud_pagination_limit' => $limit, + ] + ); } /** - * crud is in readonly mode - * @var bool + * [setResultData description] + * @param array $data [description] */ - public $readOnly = false; + public function setResultData(array $data): void + { + $this->resultData = $data; + } /** - * Returns the form HTML code for showing an existing entry without editing function. Will make sure the given data is compliant to the form's and model's configuration - * @param string|int $primaryKey [description] + * [export description] + * @param bool $raw [enables raw export] * @return void + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + * @throws noticeException */ - public function show($primaryKey) { - - $this->readOnly = true; - - // Readonly handling is now done in makeForm - if($this->readOnly) { - // apply to all nested cruds - foreach($this->childCruds as $crud) { - $crud->readOnly = true; - } - } - - // use modified makeForm function, that allows $addSubmitButton = false (second argument) - $form = $this->makeForm($primaryKey, false); - - // Fire the form init event - $hookval = $this->eventCrudFormInit->invokeWithResult($this, $form); - - if(is_object($hookval) && $hookval instanceof \codename\core\ui\form) { - $form = $hookval; - } - - $this->getResponse()->setData('form', $form->output($this->outputFormConfig)); + public function export(bool $raw = false): void + { + // disable limit and offset temporarily + $this->allowPagination = false; + $this->rawMode = $raw; + $this->listview(); + $this->rawMode = false; + $this->allowPagination = true; } /** - * [loadFormConfig description] - * @param string $identifier [description] - * @return \codename\core\config + * Returns a list of the entries in the model and paginate, filter and order it + * @return void + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + * @throws noticeException */ - protected function loadFormConfig(string $identifier) : \codename\core\config { - - // prepare config - $config = null; - - // - // Try to retrieve cached config - // - if($this->useConfigCache) { - $cacheGroup = app::getVendor().'_'.app::getApp().'_CRUD_FORM'; - $cacheKey = $identifier; - if($cachedConfig = \codename\core\app::getCache()->get($cacheGroup, $cacheKey)) { - $config = new \codename\core\config($cachedConfig); - } - } - - // - // If config not already set by cache, get it - // - if(!$config) { - $config = new \codename\core\config\json('config/crud/form_' . $identifier . '.json'); + public function listview(): void + { + $visibleFields = $this->config->get('visibleFields'); - // Cache, if enabled. - if($this->useConfigCache) { - \codename\core\app::getCache()->set($cacheGroup, $cacheKey, $config->get()); + // Only append primarykey, if not added to visibleFields + if (!in_array($this->getMyModel()->getPrimaryKey(), $visibleFields)) { + $visibleFields[] = $this->getMyModel()->getPrimaryKey(); } - } - - return $config; - } - - /** - * Loads data from a form configuration file - * @param string $identifier - * @return crud - * @todo USE CACHE FOR CONFIGS - */ - public function useForm(string $identifier) : crud { - $this->getForm()->setId($identifier); - $formConfig = $this->loadFormConfig($identifier); + $formattedFields = []; // - // update child crud configs + // Format foreign key values as defined by the model // - if($formConfig->exists('children_config')) { - $childrenConfig = $formConfig->get('children_config'); - foreach($childrenConfig as $childName => $childConfig) { - if(isset($this->childCruds[$childName])) { - // DEBUG: \codename\core\app::getResponse()->setData('debug_crud_useform_childconfig_'.$identifier.'_'.$childName, $childConfig); - if(isset($childConfig['crud'])) { - $this->childCruds[$childName]->setConfig($childConfig['crud']); - } - if(isset($childConfig['form'])) { - $this->childCruds[$childName]->useForm($childConfig['form']); - } - } - } - } + if (!is_null($this->getMyModel()->config->get('foreign'))) { + $foreignKeys = $this->getMyModel()->config->get('foreign'); - if($formConfig->exists('tag')) { - $this->getForm()->config['form_tag'] = $formConfig->get('tag'); + $formattedFields = array_reduce(array_keys($foreignKeys), function ($carry, $key) { + // foreign keys use a formatted output field AND a data key + $carry[$key] = $key . '_FORMATTED'; + return $carry; + }, $formattedFields); } - if($formConfig->exists('fieldset')) { - foreach($formConfig->get('fieldset') as $key => $fieldset) { - $newFieldset = new fieldset(array('fieldset_name' => $key)); - foreach($formConfig->get("fieldset>{$key}>field") as $field) { - $options = array(); - $options['field_required'] = ($formConfig->exists("fieldset>{$key}>required") && in_array($field, $formConfig->get("fieldset>{$key}>required"))); - $options['field_readonly'] = ($formConfig->exists("fieldset>{$key}>readonly") && in_array($field, $formConfig->get("fieldset>{$key}>readonly"))); - - // - // CHANGED 2021-10-27: flag fields in fieldsets now use the same type/handling as root-level flag fields - // - if($field == $this->getMyModel()->table . '_flag') { - $flags = $this->getMyModel()->config->get('flag'); - if(!is_array($flags)) { - continue; - } + // + // also include "modifier" fields as _FORMATTED ones. + // + $formattedFields = array_merge( + $formattedFields, + array_reduce(array_keys($this->modifiers), function ($carry, $key) { + // use a modifier key as final field + $carry[$key] = $key; + return $carry; + }, []) + ); - $value = []; - $elements = []; + // + // Fields that are available as raw data AND as a _FORMATTED one + // + $this->getResponse()->setData('formattedFields', $formattedFields); - foreach($flags as $flagname => $flag) { - $value[$flagname] = !is_null($this->data) ? $this->getMyModel()->isFlag($flag, $this->data->getData()) : false; - $elements[] = [ - 'name' => $flagname, - 'display' => app::getTranslate()->translate('DATAFIELD.' . $field . '_' . $flagname), - 'value' => $flag - ]; - } + // + // Enable custom selection of displayed fields (columns) + // + $this->getResponse()->setData('enable_displayfieldselection', ($this->config->exists('displayFieldSelection') ? $this->config->get('displayFieldSelection') : false)); - $fielddata = array ( - 'field_name' => $this->getMyModel()->table . '_flag', - 'field_type' => 'multicheckbox', - 'field_datatype' => 'structure', - 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $field), - 'field_multiple' => true, - 'field_value' => $value, - 'field_elements' => $elements, - 'field_idfield' => 'name', - 'field_displayfield' => '{$element["display"]}', // todo: translate! - 'field_valuefield' => 'value' - ); - - if($this->readOnly - || ($this->config->exists('readonly') && is_array($this->config->get('readonly')) && in_array($field, $this->config->get('readonly'))) - || $options['field_readonly'] - ) { - $fielddata['field_readonly'] = true; - } + if ($this->config->exists('availableFields')) { + $availableFields = $this->config->get('availableFields'); + } else { + // enable ALL fields of the model to be displayed + $availableFields = $this->getMyModel()->config->get('field'); + } - if($options['field_required'] ?? false) { - $fielddata['field_required'] = true; - } + // add formatted fields to availableFields + $availableFields = array_merge($availableFields, array_keys($formattedFields)); - $c = &$this->onCreateFormfield; - if($c !== null && is_callable($c)) { - $c($fielddata); - } + // remove all disabled fields + if ($this->config->exists('disabled')) { + $availableFields = array_diff($availableFields, $this->config->get('disabled')); + } - $formField = new \codename\core\ui\field($fielddata); + // merge and kill duplicates + $availableFields = array_values(array_unique($availableFields)); - $c = &$this->onFormfieldCreated; - if($c !== null && is_callable($c)) { - $c($formField); - } + $displayFields = []; - $newFieldset->addField($formField); - continue; + // display fields are either the visibleFields (defined in config) or submitted + // in the latter case; we have to check for legitimacy first. + if ($this->getRequest()->isDefined('display_selectedfields') && $this->getResponse()->getData('enable_displayfieldselection')) { + $selectedFields = $this->getRequest()->getData('display_selectedfields'); + if (is_array($selectedFields)) { + // + // NOTE/CHANGED 2019-06-14: we have to include some more fields + // + $avFields = array_unique(array_merge($visibleFields, $availableFields)); + foreach ($selectedFields as $displayField) { + if (in_array($displayField, $avFields)) { + $displayFields[] = $displayField; } - - $newFieldset->addField($this->makeField($field, $options)); } - $this->getForm()->addFieldset($newFieldset); - } - } elseif ($formConfig->exists("field")) { - $this->fields = $formConfig->get("field"); - $this->fieldsformConfig = []; - if ($formConfig->exists('required')) { - $this->fieldsformConfig['required'] = $formConfig->get('required'); - } - if ($formConfig->exists('readonly')) { - $this->fieldsformConfig['readonly'] = $formConfig->get('readonly'); } } - // DEBUG: \codename\core\app::getResponse()->setData('debug_crud_fields_set_'.($this->getForm()->config['form_tag'] ?? 'no-tag').'_'.$identifier, $this->fields); - - return $this; - } - - /** - * Loads one object from the CRUD generator's model if the primary key is defined. - * @param string|int|null $primarykey - * @throws \codename\core\exception - */ - public function useEntry($primarykey = null) { - if(is_null($primarykey)) { - $this->getResponse()->setData('CRUD_FEEDBACK', 'ENTRY_CREATE'); - return $this; + if (count($displayFields) > 0) { + $visibleFields = $displayFields; + } else { + // add all modifier fields by default + // if no field selection provided + $visibleFields = array_merge($visibleFields, array_keys($this->modifiers)); } - $this->getResponse()->setData('CRUD_FEEDBACK', 'ENTRY_UPDATE'); - $this->data = new \codename\core\datacontainer($this->getMyModel()->load($primarykey)); - - // DEBUG: $this->getResponse()->setData('crud_'.$this->model->getIdentifier().'_entry', $this->data->getData()); - - return $this; - } - - /** - * [useData description] - * @param array $data [description] - * @return crud [description] - */ - public function useData(array $data) : crud { - $this->data = new \codename\core\datacontainer( - // $this->getMyModel()->normalizeData($data) - $data - ); - return $this; - } - - /** - * [useFormNormalizationData description] - * @return crud [description] - */ - public function useFormNormalizationData() : crud { - $this->data = new \codename\core\datacontainer($this->getMyModel()->normalizeData( $this->getFormNormalizationData() )); - foreach($this->childCruds as $crud) { - $crud->useFormNormalizationData(); - } - return $this; - } - - /** - * returns the current crud configuration - * - * @return \codename\core\config [description] - */ - public function getConfig() : \codename\core\config { - return $this->config; - } - /** - * Sends the pagination data to the response - * @return void - */ - protected function makePagination() { + if (!in_array($this->getMyModel()->getPrimaryKey(), $visibleFields)) { + $visibleFields[] = $this->getMyModel()->getPrimaryKey(); + } // - // small HACK: - // temporily add a count field - // perform the query using the given configuration - // and remove it afterwards. + // Provide some labels for frontend display // - if($this->getConfig()->get('seek') || $this->getRequest()->getData('crud_stats_async')) { - $count = null; - } else { - $start = microtime(true); - $count = (int) $this->getMyModel()->getCount(); - $end = microtime(true); + $fieldLabels = []; + foreach (array_merge($availableFields, $visibleFields, $formattedFields) as $field) { + if (!is_string($field)) { + continue; + } + $fieldLabels[$field] = app::getTranslate()->translate('DATAFIELD.' . $field); + } + foreach ($availableFields as $field) { + if ($fieldLabels[$field] ?? false) { + $fieldLabels[$field] = app::getTranslate()->translate('DATAFIELD.' . $field); + } + } + $this->getResponse()->setData('labels', $fieldLabels); - // DEBUG! - $this->getResponse()->setData('_count_time', ($end-$start)); + $this->applyFilters(); + + if ($this->allowPagination) { + $this->makePagination(); } - // default value, if none of the below works: - $page = 1; - if($this->getRequest()->isDefined('crud_pagination_page')) { - // explicit page request - $page = (int) $this->getRequest()->getData('crud_pagination_page'); - } else if($this->getRequest()->isDefined('crud_pagination_page_prev')) { - // fallback to previous page value, if page hasn't been submitted - $page = (int) $this->getRequest()->getData('crud_pagination_page_prev'); + if ($this->crudSeekOverridePkeyOrder ?? false) { + // Seek-mode order hack + // following ordering happens during the runtime below + $this->getMyModel()->addOrder($this->getMyModel()->getPrimaryKey(), $this->crudSeekOverridePkeyOrder); } - if($this->getRequest()->isDefined('crud_pagination_limit')) { - $limit = (int) $this->getRequest()->getData('crud_pagination_limit'); - } else { - $limit = $this->config->get("pagination>limit", 10); + foreach ($this->config->get("order") as $order) { + $this->getMyModel()->addOrder($order['field'], $order['direction']); } - if($this->getConfig()->get('seek') || $this->getRequest()->getData('crud_stats_async')) { - $pages = null; + + if ($this->getConfig()->exists('export>_security>group')) { + if ($enableExport = app::getAuth()->memberOf($this->getConfig()->get('export>_security>group'))) { + $this->getResponse()->setData('export_types', $this->getConfig()->get('export>allowedTypes')); + } + $this->getResponse()->setData('enable_export', $enableExport); } else { - $pages = ($limit==0||$count==0) ? 1 : ceil($count / $limit); + $this->getResponse()->setData('enable_export', false); } - // when not in seek mode (normal mode), limit last page to max. page available - if(!$this->getConfig()->get('seek') && !$this->getRequest()->getData('crud_stats_async')) { - // pagination limit change with present page param, that is out of range: - if($page > $pages) { - $page = $pages; - } - } - - if($this->getConfig()->get('seek') === true) { - // - // Alternative pagination method: Seeking! - // - $firstId = $this->getRequest()->getData('crud_pagination_first_id'); - $lastId = $this->getRequest()->getData('crud_pagination_last_id'); - $seekMode = $this->getRequest()->getData('crud_pagination_seek') ?? 0; - - $ordering = 'ASC'; // is this really our default? - if($this->config->get("order")) { - foreach ($this->config->get("order") as $order) { - if($order['field'] === $this->getMyModel()->getPrimarykey()) { - $ordering = $order['direction']; - } - } - } - - // stable position - if($firstId && $seekMode == 0) { - $operator = $ordering === 'ASC' ? '>=' : '<='; - // $this->getResponse()->setData('seek_debug', "{$this->getMyModel()->getPrimarykey()} $operator $firstId"); - $this->getMyModel()->addFilter($this->getMyModel()->getPrimarykey(), $firstId, $operator); - } - - // we're moving backwards - if($firstId && $seekMode < 0) { - $operator = $ordering === 'ASC' ? '<' : '>'; - // $this->getResponse()->setData('seek_debug', "{$this->getMyModel()->getPrimarykey()} $operator $firstId"); - $this->getMyModel()->addFilter($this->getMyModel()->getPrimarykey(), $firstId, $operator); - $this->crudSeekOverridePkeyOrder = $ordering === 'ASC' ? 'DESC' : 'ASC'; // enable overriding the other ordering... - } - - // we're moving forward - if($lastId && $seekMode > 0) { - $operator = $ordering === 'ASC' ? '>' : '<'; - // $this->getResponse()->setData('seek_debug', "{$this->getMyModel()->getPrimarykey()} $operator $lastId"); - $this->getMyModel()->addFilter($this->getMyModel()->getPrimarykey(), $lastId, $operator); - } - - $this->getMyModel()->setLimit($limit); - + if ($this->getConfig()->exists('import>_security>group')) { + if ($enableImport = app::getAuth()->memberOf($this->getConfig()->get('import>_security>group'))) { + // $this->getResponse()->setData('export_types', $this->getConfig()->get('export>allowedTypes')); + } + $this->getResponse()->setData('enable_import', $enableImport); } else { - if($pages > 1 || $this->getRequest()->getData('crud_stats_async')) { - $this->getMyModel()->setLimit($limit)->setOffset(($page-1) * $limit); - } + $this->getResponse()->setData('enable_import', false); } - // Response - $this->getResponse()->addData( - array( - 'crud_pagination_seek_enabled' => $this->getConfig()->get('seek') === true, - 'crud_pagination_count' => $count, - 'crud_pagination_page' => $page, - 'crud_pagination_pages' => $pages, - 'crud_pagination_limit' => $limit - ) - ); - return; - } + $fieldActions = $this->config->get("action>field") ?? []; + $filters = $this->config->get('visibleFilters', []); + // merge-in provided filters + $filters = array_merge($filters, $this->providedFilters); - /** - * [protected description] - * @var bool - */ - protected $crudSeekOverridePkeyOrder = null; + // + // build a form from filters + // + $filterForm = null; - /** - * Resolve a datatype to a foreced display type - * @param string $datatype - * @return string - */ - public function getDisplaytype(string $datatype) : string { - return self::getDisplaytypeStatic($datatype); - } + if (count($filters) > 0) { + $filterForm = new form([ + 'form_id' => 'filterform', + 'form_method' => 'post', + 'form_action' => '', + ]); - /** - * [getDisplaytypeStatic description] - * @param string $datatype [description] - * @return string [description] - */ - public static function getDisplaytypeStatic(string $datatype) : string { - switch($datatype) { - case 'structure_address': - return 'structure_address'; - case 'structure_text_telephone': - return 'structure_text_telephone'; - case 'structure': - return 'structure'; - case 'boolean': - return 'yesno'; - case 'text_date': - return 'date'; - case 'text_date_birthdate': - return 'date'; - case 'text_timestamp': - return 'timestamp'; - // - // CHANGED 2020-05-26: relativetime field detection/determination - // moved to here from field class - // - case 'text_datetime_relative': - return 'relativetime'; - default: - return 'input'; - break; - } - } + $filterForm->setFormRequest($this->getRequest()->getData(self::CRUD_FILTER_IDENTIFIER) ?? []); - /** - * list of fields that are configured - * to just provide a basic configuration - * and skip unnecessary stuff (e.g. FKEY value fetching) - * @var string[] - */ - protected $customizedFields = []; + foreach ($filters as $filterSpecifier => $filterConfig) { + $specifier = explode('.', $filterSpecifier); + $useModel = $this->getMyModel(); + $fName = $specifier[count($specifier) - 1]; - /** - * function for making fields, independent of the current crud model - * @param \codename\core\model $model [description] - * @param string $field [description] - * @param array $options [description] - * @return field [description] - */ - public function makeFieldForeign(\codename\core\model $model, string $field, array $options = []) : field { - // load model config for simplicity - $modelconfig = $model->config->get(); - - // Error if field not in model - if(!in_array($field, $model->getFields())) { - throw new \codename\core\exception(self::EXCEPTION_MAKEFIELD_FIELDNOTFOUNDINMODEL, \codename\core\exception::$ERRORLEVEL_ERROR, $field); - } - - // Create basic formfield array - $fielddata = array( - 'field_id' => $field, - 'field_name' => $field, - 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $field ), - 'field_description' => app::getTranslate()->translate('DATAFIELD.' . $field . '_DESCRIPTION' ), - 'field_type' => 'input', - 'field_required' => $options['field_required'] ?? false, - 'field_placeholder' => app::getTranslate()->translate('DATAFIELD.' . $field ), - 'field_multiple' => false, - 'field_readonly' => $options['field_readonly'] ?? false - ); - - // Get the displaytype of this field - if (array_key_exists('datatype', $modelconfig) && array_key_exists($field, $modelconfig['datatype'])) { - $fielddata['field_type'] = $this->getDisplaytype($modelconfig['datatype'][$field]); - $fielddata['field_datatype'] = $modelconfig['datatype'][$field]; - } - - if($fielddata['field_type'] == 'yesno') { - $fielddata['field_type'] = 'select'; - $fielddata['field_displayfield'] = '{$element[\'field_name\']}'; - $fielddata['field_valuefield'] = 'field_value'; - - // NOTE: Datatype for this kind of pseudo-boolean field must be null or so - // because the boolean validator really needs a bool. - $fielddata['field_datatype'] = null; - $fielddata['field_elements'] = array( - array( - 'field_value' => true, - 'field_name' => 'Ja' - ), - array( - 'field_value' => false, - 'field_name' => 'Nein' - ) - ); - } - - // Modify field to be a reference dropdown - if(array_key_exists('foreign', $modelconfig) && array_key_exists($field, $modelconfig['foreign'])) { - if(!app::getValidator('structure_config_modelreference')->reset()->isValid($modelconfig['foreign'][$field])) { - throw new \codename\core\exception(self::EXCEPTION_MAKEFIELD_INVALIDREFERENCEOBJECT, \codename\core\exception::$ERRORLEVEL_ERROR, $modelconfig['foreign'][$field]); - } - - $foreign = $modelconfig['foreign'][$field]; - - $elements = $this->getModel($foreign['model'], $foreign['app'] ?? app::getApp()); - - // - // skip basic model setup, if we're using the remote api interface anyways. - // - if(!($elements instanceof \codename\rest\model\exposesRemoteApiInterface) || !isset($foreign['remote_source'])) { - if(array_key_exists('order', $foreign) && is_array($foreign['order'])) { - foreach ($foreign['order'] as $order) { - if(!app::getValidator('structure_config_modelorder')->reset()->isValid($order)) { - throw new \codename\core\exception(self::EXCEPTION_MAKEFIELD_INVALIDORDEROBJECT, \codename\core\exception::$ERRORLEVEL_ERROR, $order); - } - $elements->addOrder($order['field'], $order['direction']); + if (count($specifier) == 2) { + // we have a model/table reference + $useModel = $this->getModel($specifier[0]); } - } - if(array_key_exists('filter', $foreign) && is_array($foreign['filter'])) { - foreach ($foreign['filter'] as $filter) { - if(!app::getValidator('structure_config_modelfilter')->reset()->isValid($filter)) { - throw new \codename\core\exception(self::EXCEPTION_MAKEFIELD_INVALIDFILTEROBJECT, \codename\core\exception::$ERRORLEVEL_ERROR, $filter); - } - if($filter['field'] == $elements->getIdentifier() . '_flag') { - if($filter['operator'] == '=') { - $elements->withFlag($elements->config->get('flag>'.$filter['value'])); - } else if($filter['operator'] == '!=') { - $elements->withoutFlag($elements->config->get('flag>'.$filter['value'])); - } else { - throw new \codename\core\exception(self::EXCEPTION_MAKEFIELD_FILTER_FLAG_INVALIDOPERATOR, \codename\core\exception::$ERRORLEVEL_ERROR, $filter); - } - } else { - $elements->addFilter($filter['field'], $filter['value'], $filter['operator']); - } + // field is a foreign key + if (!($filterConfig['wildcard'] ?? false) && in_array($fName, $useModel->config->get('field'))) { + $field = $this->makeFieldForeign($useModel, $fName, $filterConfig); // options? + } elseif ($filterConfig['config']['field_config'] ?? false) { + $fieldData = array_merge( + [ + 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $fName), + 'field_name' => $filterSpecifier, + 'field_type' => 'input', + ], + $filterConfig['config']['field_config'] + ); + $field = new field($fieldData); + } else { + // wildcard, no normalization needed + $field = new field([ + 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $fName), + 'field_name' => $filterSpecifier, + 'field_type' => 'input', + ]); } - } - } - - $fielddata['field_type'] = 'select'; - $fielddata['field_displayfield'] = $foreign['display']; - $fielddata['field_valuefield'] = $foreign['key']; - if($elements instanceof \codename\rest\model\exposesRemoteApiInterface && isset($foreign['remote_source'])) { - // $fielddata['field_elements'] = $elements->search()->getResult(); - $apiEndpoint = $elements->getExposedApiEndpoint(); - $fielddata['field_remote_source'] = $apiEndpoint; - - $remoteSource = $foreign['remote_source'] ?? []; - - // - // if(array_key_exists($foreign['model'], $defaultRemoteApiFilters)) { - // $field['field_remote_source_parameter'] = [ - // 'filter' => array_merge($defaultRemoteApiFilters[$foreign['model']], [] /*($foreign['filter'] ?? [])*/ ) - // ]; - // } - - $filterKeys = []; - foreach($remoteSource['filter_key'] as $filterKey => $filterData) { - if(is_array($filterData)) { - foreach($filterData as $filterDataKey => $filterDataData) { - $filterKeys[$filterKey][$filterDataData] = true; - } - } else { - $filterKeys[$filterData] = true; - } + $filterForm->addField($field); } + } - $fielddata['field_remote_source_filter_key'] = $filterKeys; + // + // NOTE/EXPERIMENTAL: + // if $visibleFields contains one or more elements that are arrays + // (e.g., object-path-style fields) + // this may not work properly in some cases? + // + if (count($this->columnOrder) > 0) { + $visibleFields = array_values(array_unique(array_merge(array_intersect($this->columnOrder, $visibleFields), $visibleFields), SORT_REGULAR)); + } else { + $visibleFields = array_values(array_unique($visibleFields, SORT_REGULAR)); + } - // - // Explicit Filter Key - // for retrieving an already set, unique and strictly defined value - // - if($remoteSource['explicit_filter_key'] ?? false) { - $fielddata['field_remote_source_explicit_filter_key'] = $remoteSource['explicit_filter_key']; - } + $resultData = $this->resultData ?? $this->getMyModel()->search()->getResult(); - /* - if(array_key_exists($foreign['model'], $remoteApiFilterKeys)) { - $field['field_remote_source_filter_key'] = $remoteSource['filter_key']; - } - */ - $fielddata['field_remote_source_parameter'] = $remoteSource['parameters'] ?? []; - $fielddata['field_remote_source_display_key'] = $remoteSource['display_key'] ?? null; - $fielddata['field_remote_source_links'] = $foreign['remote_source']['links'] ?? []; - $fielddata['field_valuefield'] = $foreign['key']; - $fielddata['field_displayfield'] = $foreign['key']; // $defaultDisplayField[$foreign['model']] ?? $foreign['key']; - - } else { - if(!in_array($field, $this->customizedFields)) { - $fielddata['field_elements'] = $elements->search()->getResult(); - } - } - - // if(array_key_exists('datatype', $modelconfig) && array_key_exists($field, $modelconfig['datatype']) && $modelconfig['datatype'][$field] == 'structure') { - // $fielddata['field_multiple'] = true; - // } - - // - // by default, we allow multiselect - // - $multiple = true; - if(array_key_exists('field_multiple', $options)) { - $multiple = $options['field_multiple']; - } else if(array_key_exists('multiple', $options)) { - $multiple = $options['multiple']; - } - - if($multiple) { - $fielddata['field_datatype'] = 'structure'; - $fielddata['field_multiple'] = $multiple; - } - - if($elementDatatype = $modelconfig['datatype'][$field] ?? false) { + // + // Seek mode runtime ordering + // + if ($this->crudSeekOverridePkeyOrder ?? false) { // - // if multiselect, provide element datatype for correct conversions + // Stable usort based on main models' PKEY + // this is done in reverse, as we previously changed core ordering in ":: makePagination" // - if($multiple) { - $fielddata['field_element_datatype'] = $elementDatatype; - } else { - $fielddata['field_datatype'] = $elementDatatype; - } - } - - } - - $c = &$this->onCreateFormfield; - if($c !== null && is_callable($c)) { - $c($fielddata); - } - - $field = new field($fielddata); - $field->setType('compact'); + self::stable_usort($resultData, function ($a, $b) { + // + // NOTE: we use the spaceship operator here, which outputs -1, 0 or 1 depending on value equality + // and we finally multiply it by -1 to re-gain the old/original PKEY ordering + // + return ($a[$this->getMyModel()->getPrimaryKey()] <=> $b[$this->getMyModel()->getPrimaryKey()]) + * + ($this->crudSeekOverridePkeyOrder === 'ASC' ? -1 : 1); + }); + } - $c = &$this->onFormfieldCreated; - if($c !== null && is_callable($c)) { - $c($field); - } - // Add the field to the form - return $field; - } + if (count($this->resultsetModifiers) > 0) { + foreach ($this->resultsetModifiers as $modifier) { + $resultData = $modifier($resultData); + } + } - /** - * Creates the field instance for the given field and adds information to it. - * @param string $field [description] - * @param array $options [description] - * @throws \codename\core\exception - * @return field - */ - public function makeField(string $field, array $options = array()) : field { - // load model config for simplicity - $modelconfig = $this->getMyModel()->config->get(); + // Send data to the response + if ($this->rawMode) { + $this->getResponse()->setData('rows', $resultData); + } else { + $this->getResponse()->setData('rows', $this->makeFields($resultData, $visibleFields)); + } - // Error if field not in model - if(!in_array($field, $this->getMyModel()->getFields())) { - throw new \codename\core\exception(self::EXCEPTION_MAKEFIELD_FIELDNOTFOUNDINMODEL, \codename\core\exception::$ERRORLEVEL_ERROR, $field); - } - - // Create basic formfield array - $fielddata = array( - 'field_id' => $field, - 'field_name' => $field, - 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $field ), - 'field_description' => app::getTranslate()->translate('DATAFIELD.' . $field . '_DESCRIPTION' ), - 'field_type' => 'input', - 'field_required' => $options['field_required'] ?? false, - 'field_placeholder' => app::getTranslate()->translate('DATAFIELD.' . $field ), - 'field_multiple' => false, - 'field_readonly' => $options['field_readonly'] ?? false - ); + $this->getResponse()->setData('filterform', $filterForm?->output(true)); - // Get the displaytype of this field - if (array_key_exists('datatype', $modelconfig) && array_key_exists($field, $modelconfig['datatype'])) { - $fielddata['field_type'] = $this->getDisplaytype($modelconfig['datatype'][$field]); - $fielddata['field_datatype'] = $modelconfig['datatype'][$field]; - } + $this->getResponse()->setData('topActions', $this->prepareActionsOutput($this->config->get("action>top") ?? [])); + $this->getResponse()->setData('bulkActions', $this->prepareActionsOutput($this->config->get("action>bulk") ?? [])); + $this->getResponse()->setData('elementActions', $this->prepareActionsOutput($this->config->get("action>element") ?? [])); + $this->getResponse()->setData('fieldActions', $this->prepareActionsOutput($fieldActions) ?? []); + $this->getResponse()->setData('visibleFields', $visibleFields); + $this->getResponse()->setData('availableFields', $availableFields); - if($fielddata['field_type'] == 'yesno') { - $fielddata['field_type'] = 'select'; - $fielddata['field_displayfield'] = '{$element[\'field_name\']}'; - $fielddata['field_valuefield'] = 'field_value'; + $this->getResponse()->setData('crud_filter_identifier', self::CRUD_FILTER_IDENTIFIER); + $this->getResponse()->setData('filters_used', $filterForm?->normalizeData($filterForm->getData())); + $this->getResponse()->setData('enable_search_bar', $this->config->exists("visibleFilters>_search")); + $this->getResponse()->setData('modelinstance', $this->getMyModel()); - // NOTE: Datatype for this kind of pseudo-boolean field must be null or so - // because the boolean validator really needs a bool. - $fielddata['field_datatype'] = null; - $fielddata['field_elements'] = array( - array( - 'field_value' => true, - 'field_name' => 'Ja' - ), - array( - 'field_value' => false, - 'field_name' => 'Nein' - ) - ); + // editable mode: + if ($this->getRequest()->getData('crud_editable')) { + $form = $this->makeForm(null, false); + $this->getResponse()->setData('formconfig', $form->output(true)); } - if($this->config->exists("required")) { - if (in_array($field, $this->config->get('required'))) { - $fielddata['field_required'] = true; - } + // + // Alternative pagination method: seek + // to provide first and last id fetched + // + if ($this->getConfig()->get('seek') === true) { + $rows = $this->getResponse()->getData('rows'); + if (is_array($rows) && count($rows) !== 0) { + $first = reset($rows); + $last = end($rows); + $this->getResponse()->addData([ + 'crud_pagination_first_id' => $first[$this->getMyModel()->getPrimaryKey()], + 'crud_pagination_last_id' => $last[$this->getMyModel()->getPrimaryKey()], + ]); + } } + } - if(!is_null($this->data)) { - $fielddata['field_value'] = ($this->data->isDefined($field) ? $this->getMyModel()->exportField(new \codename\core\value\text\modelfield($field), $this->data->getData($field)) : null); - } + /** + * stable usort function + * @param array $array + * @param $value_compare_func + * @return bool + */ + protected static function stable_usort(array &$array, $value_compare_func): bool + { + $index = 0; + foreach ($array as &$item) { + $item = [$index++, $item]; + } + $result = usort($array, function ($a, $b) use ($value_compare_func) { + $result = call_user_func($value_compare_func, $a[1], $b[1]); + return $result == 0 ? $a[0] - $b[0] : $result; + }); + foreach ($array as &$item) { + $item = $item[1]; + } + return $result; + } - // Set primary key field hidden - if($field == $this->getMyModel()->getPrimarykey()) { - // if(($options['field_readonly'] ?? false) == true) { - // $fielddata['field_type'] = 'infopanel'; - // } else { - $fielddata['field_type'] = 'hidden'; - // } + /** + * This method loops all the given datasets in $rows and the given $fields. + * The method generates a new output array and tries to overwrite field values by using getFieldoutput() + * @param array $rows + * @param array $fields + * @return array + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + * @throws noticeException + */ + protected function makeFields(array $rows, array $fields): array + { + // return dataset, if there is no modifier (row, field) and no foreign key data to be fetched + if (count($this->rowModifiers) == 0 + && count($this->modifiers) == 0 + && is_null($this->getMyModel()->config->get('foreign')) + ) { + return $rows; } - // Decode object datatypes - if(strpos($fielddata['field_type'], 'bject_') !== false) { - $fielddata['field_value'] = app::object2array(json_decode($fielddata['field_value'])); - } - if ($this->getMyModel()->config->exists("required") && in_array($field, $this->getMymodel()->config->get("required"))) { - $fielddata['field_required'] = true; + $searchForFields = $fields; + if (count($this->modifiers) > 0) { // merge or replace? + $searchForFields = array_merge($searchForFields, array_keys($this->modifiers)); } - // Modify field to be a reference dropdown - if(array_key_exists('foreign', $modelconfig) && array_key_exists($field, $modelconfig['foreign'])) { - if(!app::getValidator('structure_config_modelreference')->reset()->isValid($modelconfig['foreign'][$field])) { - throw new \codename\core\exception(self::EXCEPTION_MAKEFIELD_INVALIDREFERENCEOBJECT, \codename\core\exception::$ERRORLEVEL_ERROR, $modelconfig['foreign'][$field]); + $myRows = []; + foreach ($rows as $row) { + if ($this->provideRawData) { + $object = $row; + } else { + $object = []; } - $foreign = $modelconfig['foreign'][$field]; - - $elements = $this->getModel($foreign['model'], $foreign['app'] ?? app::getApp()); - - // - // skip basic model setup, if we're using the remote api interface anyways. - // - if(!($elements instanceof \codename\rest\model\exposesRemoteApiInterface) || !isset($foreign['remote_source'])) { - if(array_key_exists('order', $foreign) && is_array($foreign['order'])) { - foreach ($foreign['order'] as $order) { - if(!app::getValidator('structure_config_modelorder')->reset()->isValid($order)) { - throw new \codename\core\exception(self::EXCEPTION_MAKEFIELD_INVALIDORDEROBJECT, \codename\core\exception::$ERRORLEVEL_ERROR, $order); - } - $elements->addOrder($order['field'], $order['direction']); - } - } - - if(array_key_exists('filter', $foreign) && is_array($foreign['filter'])) { - foreach ($foreign['filter'] as $filter) { - if(!app::getValidator('structure_config_modelfilter')->reset()->isValid($filter)) { - throw new \codename\core\exception(self::EXCEPTION_MAKEFIELD_INVALIDFILTEROBJECT, \codename\core\exception::$ERRORLEVEL_ERROR, $filter); - } - if($filter['field'] == $elements->getIdentifier() . '_flag') { - if($filter['operator'] == '=') { - $elements->withFlag($elements->config->get('flag>'.$filter['value'])); - } else if($filter['operator'] == '!=') { - $elements->withoutFlag($elements->config->get('flag>'.$filter['value'])); - } else { - throw new \codename\core\exception(self::EXCEPTION_MAKEFIELD_FILTER_FLAG_INVALIDOPERATOR, \codename\core\exception::$ERRORLEVEL_ERROR, $filter); - } - } else { - $elements->addFilter($filter['field'], $filter['value'], $filter['operator']); - } - } - } + if (count($this->modifiers) > 0 || !is_null($this->getMyModel()->config->get('foreign'))) { + foreach ($searchForFields as $field) { + $o = $this->getFieldoutput($row, $field); + // + // field is an array: object path + // + if (is_array($field)) { + $object = deepaccess::set($object, $field, $o[0]); + continue; + } + // @NOTE: we're differentiating between a pre-formatted and a raw value here: + // if array index 1 is set, this is the formatted value. + if (array_key_exists(1, $o)) { + $object[$field . '_FORMATTED'] = $o[1]; + } + $object[$field] = $o[0]; + } + } else { + $object = $row; } - $fielddata['field_type'] = 'select'; - $fielddata['field_displayfield'] = $foreign['display']; - $fielddata['field_valuefield'] = $foreign['key']; - - if($elements instanceof \codename\rest\model\exposesRemoteApiInterface && isset($foreign['remote_source'])) { - // $fielddata['field_elements'] = $elements->search()->getResult(); - $apiEndpoint = $elements->getExposedApiEndpoint(); - $fielddata['field_remote_source'] = $apiEndpoint; - - $remoteSource = $foreign['remote_source'] ?? []; - - // - // if(array_key_exists($foreign['model'], $defaultRemoteApiFilters)) { - // $field['field_remote_source_parameter'] = [ - // 'filter' => array_merge($defaultRemoteApiFilters[$foreign['model']], [] /*($foreign['filter'] ?? [])*/ ) - // ]; - // } - - $filterKeys = []; - foreach($remoteSource['filter_key'] as $filterKey => $filterData) { - if(is_array($filterData)) { - foreach($filterData as $filterDataKey => $filterDataData) { - $filterKeys[$filterKey][$filterDataData] = true; - } - } else { - $filterKeys[$filterData] = true; + if (count($this->rowModifiers) > 0) { + $attributes = []; + foreach ($this->rowModifiers as $rowModifier) { + $modifierOutput = $rowModifier($row); + if (is_array($modifierOutput)) { + $attributes = array_merge_recursive($attributes, $modifierOutput); + } } - } - - $fielddata['field_remote_source_filter_key'] = $filterKeys; - - // - // Explicit Filter Key - // for retrieving an already set, unique and strictly defined value - // - if($remoteSource['explicit_filter_key'] ?? false) { - $fielddata['field_remote_source_explicit_filter_key'] = $remoteSource['explicit_filter_key']; - } - - /* - if(array_key_exists($foreign['model'], $remoteApiFilterKeys)) { - $field['field_remote_source_filter_key'] = $remoteSource['filter_key']; - } - */ - $fielddata['field_remote_source_parameter'] = $remoteSource['parameters'] ?? []; - $fielddata['field_remote_source_display_key'] = $remoteSource['display_key'] ?? null; - $fielddata['field_remote_source_links'] = $foreign['remote_source']['links'] ?? []; - $fielddata['field_valuefield'] = $foreign['key']; - $fielddata['field_displayfield'] = $foreign['key']; // $defaultDisplayField[$foreign['model']] ?? $foreign['key']; - } else { - if(!in_array($field, $this->customizedFields)) { - $fielddata['field_elements'] = $elements->search()->getResult(); - } + $object['__modifier'] = join( + ' ', + array_map(function ($key) use ($attributes) { + if (is_bool($attributes[$key])) { + return $attributes[$key] ? $key : ''; + } + return $key . '="' . $attributes[$key] . '"'; + }, array_keys($attributes)) + ); } - if(array_key_exists('datatype', $modelconfig) && array_key_exists($field, $modelconfig['datatype']) && $modelconfig['datatype'][$field] == 'structure') { - $fielddata['field_multiple'] = true; - } + $myRows[] = $object; } + return $myRows; + } + /** + * This method will return the output value of the given $field using the data from the given $row. + * It will determine the output value in the following two situations: + * #1: The $field has been given a modifier using ->addModifier($field, $callable) + * #2: The $field has been configured to display data from another model (a.k.a. foreign key / reference) + * @param array $row + * @param array|string $field + * @return array + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + * @throws noticeException + */ + protected function getFieldoutput(array $row, array|string $field): array + { + if (is_array($field)) { + return [deepaccess::get($row, $field)]; + } - // - // nested crud / submodel - // - if($this->config->exists('children') && in_array($field, $this->config->get('children'))) { - - $childConfig = $this->model->config->get('children>'.$field); - - if($childConfig['type'] === 'foreign') { - // - // Handle nested forms - // - $fielddata['field_type'] = 'form'; + if (array_key_exists($field, $this->modifiers)) { + return [$this->modifiers[$field]($row)]; + } - // provide a sub-form config ! - // $crud = new \codename\core\ui\crud($this->getModel($foreign['model'], $foreign['app'] ?? '', $foreign['vendor'] ?? '')); - $crud = $this->childCruds[$field]; - $crud->onCreateFormfield = $this->onCreateFormfield; - $crud->onFormfieldCreated = $this->onFormfieldCreated; + if (!isset($row[$field])) { + return [null]; + } - if($this->readOnly) { - $crud->readOnly = $this->readOnly; + if ($field == $this->getMyModel()->table . '_flag') { + $flags = $this->getMyModel()->config->get("flag"); + $ret = ''; + foreach ($flags as $flagname => $flagval) { + if ($this->getMyModel()->isFlag($flagval, $row)) { + $text = app::getTranslate()->translate('DATAFIELD.' . $field . '_' . $flagname); + $ret .= '' . $text . ''; + } } + return [$row[$field], $ret]; + } - // available child config keys: - // - type (e.g. foreign) - // - field (reference field) - $childIdentifierValue = ($this->data && $this->data->isDefined($childConfig['field']) ? $this->getMyModel()->exportField(new \codename\core\value\text\modelfield($childConfig['field']), $this->data->getData($childConfig['field'])) : null); - $form = $crud->makeForm($childIdentifierValue, false); // make form without submit - - // $this->getResponse()->setData('debug_crud_form_' . $field, $crud->getFormNormalizationData()); - // $form->setFormRequest($crud->getFormNormalizationData()); + $foreignkeys = $this->getMyModel()->config->get("foreign"); + if (!is_array($foreignkeys) || !array_key_exists($field, $foreignkeys)) { + return [$row[$field]]; + } - $fielddata['form'] = $form; - $formdata = []; - foreach($form->getFields() as $field) { - $formdata[$field->getProperty('field_name')] = $field->getProperty('field_value'); - } - $fielddata['field_value'] = $formdata; - } else if($childConfig['type'] === 'collection') { - // - // Handle collections - // - $collectionConfig = $this->model->config->get('collection>'.$field); - $fielddata['field_type'] = 'table'; - $fielddata['field_datatype'] = 'structure'; + if (array_key_exists('optional', $foreignkeys[$field]) && $foreignkeys[$field]['optional'] && $row[$field] == null) { + return [$row[$field]]; + } - $crud = new \codename\core\ui\crud($this->getModel($collectionConfig['model'], $collectionConfig['app'] ?? '', $collectionConfig['vendor'] ?? '')); - $crud->onCreateFormfield = $this->onCreateFormfield; - $crud->onFormfieldCreated = $this->onFormfieldCreated; - // TODO: allow custom crud config somehow? - // $crud->setConfig('some-crud-config'); + // TODO: We may have to differentiate here + // for values which still have to be displayed in some way, + // but they're NULL. ... - $fielddata['field_rowkey'] = $crud->getMyModel()->getPrimarykey(); + $obj = $foreignkeys[$field]; - $fielddata['visibleFields'] = $crud->getConfig()->get('visibleFields'); + if ($obj['display'] != null) { + if (is_array($row[$field])) { + $vals = []; + foreach ($row[$field] as $val) { + $element = $this->getModelCached($obj['model'])->loadByUnique($obj['key'], $val); + if (count($element) > 0) { + @eval('$vals[] = "' . $obj['display'] . '";'); + } + } + $ret = implode(', ', $vals); + } else { + // $field should be $obj['key']. check dependencies, correct mistakes and do it right! + // TODO: wrap this in a try/catch statement + // bare/json datasource's may lose unique keys. fallback to null or "undefined"? + + // first: try to NOT perform an additional query + $ret = null; // default fallback value + + // NOTE: we silence E_NOTICE's in core app + // therefore, temporary override the error handler + // and throw an internal exception to catch. + // In This case, we know the eval failed, and we have to re-try. + // This will/should fail, when a specific key is missing + set_error_handler(function ($err_severity, $err_msg, $err_file, $err_line) { + throw new noticeException($err_msg, 0, $err_severity, $err_file, $err_line); + }, E_NOTICE); + + try { + $evalResult = @eval('$ret = "' . $obj['display'] . '";'); + } catch (noticeException) { + $evalResult = false; + } - $fielddata['labels'] = []; - foreach($fielddata['visibleFields'] as $field) { - $fielddata['labels'][$field] = app::getTranslate()->translate('DATAFIELD.'.$field); + // restore error handler, should be the core-app one. + restore_error_handler(); + // + // NOTE/WARNING: + // eval only returns FALSE, if there's an exception thrown internally + // as we changed the app class to no longer throw an exception on a Notice + // (e.g., if array index/key not set), we don't run into the situation + // + // so, we now check for $evalResult === null + // + // CHANGED 2021-04-14: see note above, we override the error handler temporarily + // + if (!$evalResult) { + $element = $this->getModelCached($obj['model'], $obj['app'] ?? '', $obj['vendor'] ?? '')->loadByUnique($obj['key'], $row[$field]); + if (count($element) > 0) { + @eval('$ret = "' . $obj['display'] . '";'); + } else { + $ret = null; + } + } } - - $form = $crud->makeForm(null, false); - $fielddata['form'] = $form->output(true); - } + return [$row[$field], $ret]; + } else { + return [$row[$field]]; } + } - if($this->readOnly) { - $fielddata['field_readonly'] = true; + /** + * [getModelCached description] + * @param string $model [description] + * @param string $app [description] + * @param string $vendor [description] + * @return model [description] + * @throws ReflectionException + * @throws exception + */ + protected function getModelCached(string $model, string $app = '', string $vendor = ''): model + { + $identifier = implode(',', [$model, $app, $vendor]); + if (!($this->cachedModels[$identifier] ?? false)) { + $this->cachedModels[$identifier] = $this->getModel($model, $app, $vendor); } + return $this->cachedModels[$identifier]; + } - $c = &$this->onCreateFormfield; - if($c !== null && is_callable($c)) { - $c($fielddata); + /** + * imports a previously exported dataset + * + * @param array $data [description] + * @param bool $ignorePkeys [description] + * @return void + * @throws ReflectionException + * @throws exception + */ + public function import(array $data, bool $ignorePkeys = true): void + { + foreach ($data as $dataset) { + $this->getMyModel()->reset(); + if (count($errors = $this->getMyModel()->validate($dataset)->getErrors()) > 0) { + // erroneous dataset found + throw new exception('CRUD_IMPORT_INVALID_DATASET', exception::$ERRORLEVEL_ERROR, $errors); + } } - $field = new field($fielddata); - $field->setType('compact'); - - $c = &$this->onFormfieldCreated; - if($c !== null && is_callable($c)) { - $c($field); + foreach ($data as &$dataset) { + if (($dataset[$this->getMyModel()->getPrimaryKey()] ?? false) && $ignorePkeys) { + unset($dataset[$this->getMyModel()->getPrimaryKey()]); + } + // TODO: recurse? + $this->getMyModel()->entryMake($dataset)->entrySave(); } - // Add the field to the form - return $field; - } - - /** - * Provides a way to hook into the formfield creation process - * the fielddata array used for the field-.ctor is being used as argument - * @var callable - */ - public $onCreateFormfield = null; + $this->getResponse()->setData('import_data', $data); + } /** - * Provides a way to hook into when the formfield has been created - * the created field is being used as argument - * @var callable + * Adds a top action + * @param array $action + * @return void + * @throws ReflectionException + * @throws exception */ - public $onFormfieldCreated = null; + public function addTopaction(array $action): void + { + $this->addAction('top', $action); + } /** * Adds an action button / element to the given action type. * @param string $type * @param array $action * @return void + * @throws ReflectionException + * @throws exception * @todo use really abstract and usable action value-object in here. */ - protected function addAction(string $type, array $action) { - if(count($errors = app::getValidator('structure_config_crud_action')->reset()->validate($action)) > 0) { - throw new \codename\core\exception(self::EXCEPTION_ADDACTION_INVALIDACTIONOBJECT, \codename\core\exception::$ERRORLEVEL_ERROR, $errors); + protected function addAction(string $type, array $action): void + { + if (count($errors = app::getValidator('structure_config_crud_action')->reset()->validate($action)) > 0) { + throw new exception(self::EXCEPTION_ADDACTION_INVALIDACTIONOBJECT, exception::$ERRORLEVEL_ERROR, $errors); } $config = $this->config->get(); $config['action'][$type][$action['name']] = $action; - $this->config = new \codename\core\config($config); - - return; + $this->config = new config($config); } /** - * Returns the default CRUD filter for the given filters - * @param string $field [description] - * @param bool $wildcard [description] - * @return string [description] + * Adds a bulk action + * @param array $action + * @return void + * @throws ReflectionException + * @throws exception */ - protected function getDefaultoperator(string $field, bool $wildcard = false) : string { - switch($this->getMyModel()->getFieldtype(new \codename\core\value\text\modelfield($field))) { - case 'number_natural' : - return '='; - break; - default: - return $wildcard ? 'ILIKE' : '='; - break; - } + public function addBulkaction(array $action): void + { + $this->addAction('bulk', $action); } /** - * Will return the filterable string that is used for the given field's datatype - * @param mixed $value [description] - * @param string $field [description] - * @param bool $wildcard [description] - * @return mixed [description] + * Adds an element action + * @param array $action + * @return void + * @throws ReflectionException + * @throws exception */ - protected function getFilterstring($value, string $field, bool $wildcard = false) { - if(is_array($value)) { - return $value; - } - switch($this->getMyModel()->getFieldtype(new \codename\core\value\text\modelfield($field))) { - case 'number_natural' : - return $value; - break; - default: - return $wildcard ? "%{$value}%" : "{$value}"; - break; - } - } - - - /** - * provides a custom filter option - * @param string $name [description] - * @param array $config [description] - * @param callable $cb [description] - * @return [type] [description] - */ - public function provideFilter(string $name, array $config, callable $cb) { - $this->providedFilters[$name] = [ - 'config' => $config, - 'callback' => $cb - ]; + public function addElementaction(array $action): void + { + $this->addAction('element', $action); } /** - * customized, provided filters - * @var array + * Returns a form HTML code for creating an object. After validating the data in the form class AND validating the data in the model class, we will try to store the data in the model. + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception */ - protected $providedFilters = []; + public function create(): void + { + $this->getResponse()->setData('context', 'crud'); + $form = $this->makeForm(); - /** - * Will apply defaultFilter properties to the model instance of this CRUD generator - * @return void - */ - protected function applyFilters() { - if(!$this->getRequest()->isDefined(self::CRUD_FILTER_IDENTIFIER)) { + // Fire the form init event + $hookval = $this->eventCrudFormInit->invokeWithResult($this, $form); + if ($hookval instanceof form) { + $form = $hookval; + } + + if (!$form->isSent()) { + $this->getResponse()->setData('form', $form->output($this->outputFormConfig)); return; } - $filters = $this->getRequest()->getData(self::CRUD_FILTER_IDENTIFIER); - if(!is_array($filters)) { + + $data = $this->getMyModel()->normalizeData($this->getFormNormalizationData()); + + // Fire the before validation event + $newData = $this->eventCrudBeforeValidation->invokeWithResult($this, $data); + if (is_array($newData)) { + $data = $newData; + } + + // form validation before model validation + if (!$form->isValid()) { + $this->getResponse()->setStatus(response::STATUS_INTERNAL_ERROR); + $this->getResponse()->setData('errors', $form->getErrorstack()->getErrors()); + $this->getResponse()->setData('view', 'validation_error'); return; } - if(array_key_exists('search', $filters) && $filters['search'] != '') { - if($this->config->exists("visibleFilters>_search") && is_array($this->config->get("visibleFilters>_search"))) { - $filterCollection = array(); - foreach($this->config->get("visibleFilters>_search>fields") as $field) { - $filterCollection[] = array('field' => $field, 'value' => $this->getFilterstring($filters['search'], $field, true), 'operator' => 'ILIKE'); - } - $this->getMyModel()->addDefaultFilterCollection($filterCollection, 'OR'); - } - // do NOT return; as we may use other filters, too. Why tho? - // return; + if (!$this->getMyModel()->isValid($data)) { + $this->getResponse()->setStatus(response::STATUS_INTERNAL_ERROR); + $this->getResponse()->setData('errors', $this->getMyModel()->getErrors()); + $this->getResponse()->setData('view', 'save_error'); + return; } - foreach($filters as $key => $value) { - // exclude search key here, as we're not returning after a wildcard search anymore - if($key === 'search') { - continue; - } + // Fire hook after successful validation + $this->eventCrudAfterValidation->invoke($this, $data); - if($providedFilter = $this->providedFilters[$key] ?? false) { - $providedFilter['callback']($this, $value); - continue; - } + // Fire hook for additional validators + $errorResults = $this->eventCrudValidation->invokeWithAllResults($this, $data); - if($key == $this->getMyModel()->getIdentifier() . '_flag') { - if(is_array($value)) { - foreach($value as $flagval) { - $this->getMyModel()->withDefaultFlag($this->getFilterstring($flagval, $key, false)); - } - } else { - $this->getMyModel()->withDefaultFlag($this->getFilterstring($value, $key, false)); - } - } else { - if(is_array($value) && $this->model->config->exists("datatype>".$key) && in_array($this->model->config->get("datatype>".$key), array('text_timestamp', 'text_date'))) { - $this->getMyModel()->addDefaultfilter($key, $this->getFilterstring($value[0], $key, false), '>='); - $this->getMyModel()->addDefaultfilter($key, $this->getFilterstring($value[1], $key, false), '<='); - } else { - $wildcard = $this->config->exists("visibleFilters>".$key.">wildcard") && ($this->config->get("visibleFilters>".$key.">wildcard") == true); - $operator = $this->config->exists("visibleFilters>".$key.">operator") ? $this->config->get("visibleFilters>".$key.">operator") : $this->getDefaultoperator($key, $wildcard); - $this->getMyModel()->addDefaultfilter($key, $this->getFilterstring($value, $key, $wildcard), $operator); - } + $errors = []; + foreach ($errorResults as $errorCollection) { + if (count($errorCollection) > 0) { + $errors = array_merge($errors, $errorCollection); } } - return; + + if (count($errors) > 0) { + $this->getResponse()->setStatus(response::STATUS_INTERNAL_ERROR); + $this->getResponse()->setData('errors', $errors); + $this->getResponse()->setData('view', 'save_error'); + return; + } + + $this->eventCrudBeforeSave->invoke($this, $data); + + $model = $this->getMyModel(); + if ($model instanceof model\schematic\sql || $model instanceof model\abstractDynamicValueModel) { + $model->saveWithChildren($data); + } else { + throw new exception('CRUD_CREATE_WRONG_MODEL', exception::$ERRORLEVEL_FATAL); + } + + // eventCrudBeforeSave MUST NOT modify data, due to crud mechanics. Data might be modified in eventCrudBeforeValidation or so + $this->eventCrudSuccess->invoke($this, $data); + + $this->getResponse()->setData($this->getMyModel()->getPrimaryKey(), $this->getMyModel()->lastInsertId()); + + $this->getResponse()->setData('view', 'crud_success'); } /** - * This method loops all the given datasets in $rows and the given $fields. - *
The method generates a new output array and tries to overwrite field values by using getFieldoutput() - * @param array $rows - * @param array $fields - * @return array + * @return void + * @throws exception */ - protected function makeFields(array $rows, array $fields) : array { - - // simply return dataset, if there is no modifier (row, field) and no foreign key data to be fetched - if(count($this->rowModifiers) == 0 - && count($this->modifiers) == 0 - && is_null($this->getMyModel()->config->get('foreign')) - ) { - return $rows; + public function bulkDelete(): void + { + if (!$this->getRequest()->isDefined($this->getMyModel()->getPrimaryKey())) { + throw new exception('CRUD_BULK_DELETE_PRIMARYKEY_UNDEFINED', exception::$ERRORLEVEL_ERROR); } + } - $searchForFields = $fields; - if(count($this->modifiers) > 0) { // merge or replace? - $searchForFields = array_merge($searchForFields, array_keys($this->modifiers)); - } + /** + * [bulkEdit description] + * @return void + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function bulkEdit(): void + { + if ($this->getRequest()->isDefined('data')) { + $data = $this->getRequest()->getData('data'); - $myRows = array(); - foreach($rows as $row) { + // + // Validate + // + foreach ($data as $entry) { + // get full entry with modified delta + if ($entry[$this->getMyModel()->getPrimaryKey()] ?? false) { + $currentEntry = $this->getMyModel()->load($entry[$this->getMyModel()->getPrimaryKey()]); + } else { + $currentEntry = []; + } + $currentEntry = array_replace_recursive($currentEntry, $entry); - if($this->provideRawData) { - $object = $row; - } else { - $object = array(); - } - - if(count($this->modifiers) > 0 || !is_null($this->getMyModel()->config->get('foreign'))) { - - /* $searchForFields = $fields; - if(count($this->modifiers) > 0) { // merge or replace? - $searchForFields = array_merge($searchForFields, array_keys($this->modifiers)); - }*/ - - /* - if(!is_null($this->getMyModel()->config->get('foreign'))) { // merge or replace? - // do not add foreign keys as defaults to this one - // as it causes a lot of extra queries. - $searchForFields = array_merge($searchForFields, array_keys($this->getMyModel()->config->get('foreign'))); - } - */ - - foreach($searchForFields as $field) { - $o = $this->getFieldoutput($row, $field); - // - // field is an array: object path - // - if(is_array($field)) { - $object = \codename\core\helper\deepaccess::set($object, $field, $o[0]); - continue; - } - // @NOTE: we're differentiating between a pre-formatted and a raw value here: - // if array index 1 is set, this is the formatted value. - if(array_key_exists(1, $o)) { - $object[$field.'_FORMATTED'] = $o[1]; - $object[$field] = $o[0]; - } else { - $object[$field] = $o[0]; - } - } - } else { - $object = $row; + // TODO: validate using bulk form? + + if (!$this->getMyModel()->isValid($currentEntry)) { + $this->getResponse()->setStatus(response::STATUS_INTERNAL_ERROR); + $this->getResponse()->setData('errors', $this->getMyModel()->getErrors()); + $this->getResponse()->setData('view', 'save_error'); + return; + } } - if(count($this->rowModifiers) > 0) { - $attributes = array(); - foreach($this->rowModifiers as $rowModifier) { - $modifierOutput = $rowModifier($row); - if(is_array($modifierOutput)) { - $attributes = array_merge_recursive($attributes, $modifierOutput); + // + // Save + // + $transaction = new transaction('crud_bulk_edit', [$this->getMyModel()]); + $transaction->start(); + + $pkeyValues = []; + + foreach ($data as $entry) { + // + // TODO: how to handle delta edits on nested models? + // + $model = $this->getMyModel(); + if ($model instanceof model\schematic\sql || $model instanceof model\abstractDynamicValueModel) { + $model->saveWithChildren($entry); + } else { + throw new exception('CRUD_BULKEDIT_WRONG_MODEL', exception::$ERRORLEVEL_FATAL); } - } - $object['__modifier'] = join(' ', array_map(function($key) use ($attributes) - { - if(is_bool($attributes[$key])) - { - return $attributes[$key]?$key:''; - } - return $key.'="'.$attributes[$key].'"'; - }, array_keys($attributes)) - ); + if ($pkeyValue = $entry[$this->getMyModel()->getPrimaryKey()] ?? false) { + $pkeyValues[] = $pkeyValue; + } else { + $pkeyValues[] = $this->getMyModel()->lastInsertId(); + } } - $myRows[] = $object; + $transaction->end(); + + $this->getResponse()->setData($this->getMyModel()->getPrimaryKey(), $pkeyValues); + } else { + throw new exception('CRUD_BULK_EDIT_DATA_UNDEFINED', exception::$ERRORLEVEL_ERROR); } - return $myRows; } /** - * whether the crud_list - * should provide raw result parts - * from the model query - * @var bool + * Returns the form HTML code for editing an existing entry. Will make sure the given data is compliant to the form's and model's configuration + * @param int|string $primarykey + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception */ - protected $provideRawData = false; + public function edit(int|string $primarykey): void + { + $form = $this->makeForm($primarykey); - /** - * [setProvideRawData description] - * @param bool $state [description] - */ - public function setProvideRawData(bool $state) { - $this->provideRawData = $state; - } + // Fire the form init event + $hookval = $this->eventCrudFormInit->invokeWithResult($this, $form); - /** - * This method will return the output value of the given $field using the data from the given $row. - *
It will determine the output value by the following two situations: - *
#1: The $field has been given a modifier using ->addModifier($field, $callable) - *
#2: The $field has been configured to display data from another model (a.k.a foreign key / reference) - * @param array $row - * @param string|array $field - * @return string - */ - protected function getFieldoutput(array $row, $field) { + if ($hookval instanceof form) { + $form = $hookval; + } - if(is_array($field)) { - return [\codename\core\helper\deepaccess::get($row, $field)]; + if ($this->config->exists('action>crud_edit')) { + $this->getResponse()->setData('editActions', $this->config->get('action>crud_edit')); } - if(array_key_exists($field, $this->modifiers)) { - // if(array_key_exists($field, $row)) { - // return array($row[$field], $this->modifiers[$field]($row)); - // } else { - // } - return array($this->modifiers[$field]($row)); + if (!$form->isSent()) { + $this->getResponse()->setData('form', $form->output($this->outputFormConfig)); + return; } - if(!isset($row[$field])) { - return array(null); + // we can use $form->getData() here, but then we're receiving a lot more data (e.g., non-input or disabled fields!) + $data = $this->getMyModel()->normalizeData($this->getFormNormalizationData()); + + $newData = $this->eventCrudBeforeValidation->invokeWithResult($this, $data); + if (is_array($newData)) { + $data = $newData; } - if($field == $this->getMyModel()->table . '_flag') { - $flags = $this->getMyModel()->config->get("flag"); - $ret = ''; - foreach($flags as $flagname => $flagval) { - if($this->getMyModel()->isFlag($flagval, $row)) { - $text = app::getTranslate()->translate('DATAFIELD.' . $field . '_' . $flagname); - $ret .= "{$text}"; - } - } - return array($row[$field], $ret); + // form validation before model validation + if (!$form->isValid()) { + $this->getResponse()->setStatus(response::STATUS_INTERNAL_ERROR); + $this->getResponse()->setData('errors', $form->getErrorstack()->getErrors()); + $this->getResponse()->setData('view', 'validation_error'); + return; } - $foreignkeys = $this->getMyModel()->config->get("foreign"); - if(!is_array($foreignkeys) || !array_key_exists($field, $foreignkeys)) { - return array($row[$field]); + $this->getMyModel()->entryLoad($primarykey); + + $this->getMyModel()->entryUpdate($data); + + if (count($errors = $this->getMyModel()->entryValidate()) > 0) { + $this->getResponse()->setStatus(response::STATUS_INTERNAL_ERROR); + $this->getResponse()->setData('errors', $errors); + $this->getResponse()->setData('view', 'save_error'); + return; } - if(array_key_exists('optional', $foreignkeys[$field]) && $foreignkeys[$field]['optional']==true && $row[$field]==NULL) { - return array($row[$field]); + // Fire hook after successful validation + $this->eventCrudAfterValidation->invoke($this, $data); + + // Fire hook for additional validators + $errorResults = $this->eventCrudValidation->invokeWithAllResults($this, $data); + + $errors = []; + foreach ($errorResults as $errorCollection) { + if (count($errorCollection) > 0) { + $errors = array_merge($errors, $errorCollection); + } } - // TODO: We may have to differentiate here - // for values which still have to be displayed in some way, - // but they're NULL. ... + if (count($errors) > 0) { + $this->getResponse()->setStatus(response::STATUS_INTERNAL_ERROR); + $this->getResponse()->setData('errors', $errors); + $this->getResponse()->setData('view', 'save_error'); + return; + } - $obj = $foreignkeys[$field]; + // eventCrudBeforeSave MUST NOT modify data, due to crud mechanics. Data might be modified in eventCrudBeforeValidation or so + $this->eventCrudBeforeSave->invoke($this, $data); - if($obj['display'] != null) { - if(is_null($row[$field]) || !isset($row[$field])) { - $ret = ''; - } else if(is_array($row[$field])) { - $vals = array(); - foreach($row[$field] as $val) { - $element = $this->getModelCached($obj['model'])->loadByUnique($obj['key'], $val); - if(count($element) > 0) { - @eval('$vals[] = "' . $obj['display'] . '";'); - } - } - $ret = implode(', ', $vals); - } else { - // $field should be $obj['key']. check dependencies, correct mistakes and do it right! - // TODO: wrap this in a try/catch statement - // bare/json datasources may lose unique keys. fallback to null or "undefined"? - - // first: try to NOT perform an additional query - $element = $row; - $evalResult = false; - $ret = null; // default fallback value - - // NOTE: we silence E_NOTICEs in core app - // therefore, temporary override the error handler - // and throw an internal exception to catch. - // In This case, we know the eval failed and we have to re-try. - // This will/should fail, when a specific key is missing - set_error_handler(function ($err_severity, $err_msg, $err_file, $err_line, array $err_context) { - throw new \codename\core\NoticeException ($err_msg, 0, $err_severity, $err_file, $err_line); - }, E_NOTICE); - - try { - $evalResult = @eval('$ret = "' . $obj['display'] . '";'); - } catch (\codename\core\NoticeException $e) { - $evalResult = false; - } - - // restore error handler, should be the core-app one. - restore_error_handler(); - // - // NOTE/WARNING: - // eval only returns FALSE, if there's an exception thrown internally - // as we changed the app class to no longer throw an exception on a Notice - // (e.g. if array index/key not set), we don't run into the situation - // - // so, we now check for $evalResult === null - // - // CHANGED 2021-04-14: see note above, we override the error handler temporarily - // - if($evalResult === false) { - $element = $this->getModelCached($obj['model'], $obj['app'] ?? '', $obj['vendor'] ?? '')->loadByUnique($obj['key'], $row[$field]); - if(count($element) > 0) { - @eval('$ret = "' . $obj['display'] . '";'); - } else { - $ret = null; - } - } - } - return array($row[$field], $ret); - } else { - return array($row[$field]); + $this->getMyModel()->entryUpdate($data); + $this->getMyModel()->entrySave(); + + $this->eventCrudSuccess->invokeWithResult($this, $data); + + $this->getResponse()->setData('view', 'crud_success'); + } + + /** + * Returns the form HTML code for showing an existing entry without editing function. Will make sure the given data is compliant to the form's and model's configuration + * @param int|string $primaryKey [description] + * @return void + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function show(int|string $primaryKey): void + { + $this->readOnly = true; + + // apply to all nested cruds + foreach ($this->childCruds as $crud) { + $crud->readOnly = true; } - if($row[$field]==NULL) { - return array(null); + // use modified makeForm function, that allows $addSubmitButton = false (second argument) + $form = $this->makeForm($primaryKey, false); + + // Fire the form init event + $hookval = $this->eventCrudFormInit->invokeWithResult($this, $form); + + if ($hookval instanceof form) { + $form = $hookval; } + + $this->getResponse()->setData('form', $form->output($this->outputFormConfig)); } /** - * [protected description] - * @var \codename\core\model[] + * [useData description] + * @param array $data [description] + * @return crud [description] */ - protected $cachedModels = []; + public function useData(array $data): crud + { + $this->data = new datacontainer($data); + return $this; + } /** - * [getModelCached description] - * @param string $model [description] - * @param string $app [description] - * @param string $vendor [description] - * @return \codename\core\model [description] - */ - protected function getModelCached(string $model, string $app = '', string $vendor = ''): \codename\core\model { - $identifier = implode(',', [ $model, $app, $vendor ]); - if(!$this->cachedModels[$identifier] ?? false) { - $this->cachedModels[$identifier] = $this->getModel($model, $app, $vendor); - } - return $this->cachedModels[$identifier]; + * [useFormNormalizationData description] + * @return crud [description] + * @throws DateMalformedStringException + * @throws exception + */ + public function useFormNormalizationData(): crud + { + $this->data = new datacontainer($this->getMyModel()->normalizeData($this->getFormNormalizationData())); + foreach ($this->childCruds as $crud) { + $crud->useFormNormalizationData(); + } + return $this; } /** - * Returns the form instance of this CRUD generator instance - * @return \codename\core\ui\form + * provides a custom filter option + * @param string $name [description] + * @param array $config [description] + * @param callable $cb [description] + * @return void [type] [description] */ - public function getForm() : \codename\core\ui\form { - return $this->form; + public function provideFilter(string $name, array $config, callable $cb): void + { + $this->providedFilters[$name] = [ + 'config' => $config, + 'callback' => $cb, + ]; } /** - * Returns the private model of this instance - * @return \codename\core\model + * [setProvideRawData description] + * @param bool $state [description] */ - public function getMyModel() : \codename\core\model { - return $this->model; + public function setProvideRawData(bool $state): void + { + $this->provideRawData = $state; } + /** + * loads the crud config + * defaults to schema_table if no identifier (or '') is specified + * @param string $identifier [description] + * @return config [description] + * @throws ReflectionException + * @throws exception + */ + protected function loadConfig(string $identifier = ''): config + { + if ($identifier == '') { + $identifier = $this->getMyModel()->schema . '_' . $this->getMyModel()->table; + } + + // prepare config + $config = null; + $cacheGroup = app::getVendor() . '_' . app::getApp() . '_CRUD_CONFIG'; + $cacheKey = $identifier; + + // + // Try to retrieve cached config + // + if ($this->useConfigCache) { + if ($cachedConfig = app::getCache()->get($cacheGroup, $cacheKey)) { + $config = new config($cachedConfig); + } + } + + // + // If config not already set by cache, get it + // + if (!$config) { + $config = new json('config/crud/' . $identifier . '.json', true); + + // Cache, if enabled. + if ($this->useConfigCache) { + app::getCache()->set($cacheGroup, $cacheKey, $config->get()); + } + } + + return $config; + } } diff --git a/backend/class/field.php b/backend/class/field.php index e8eb220..c72a49d 100644 --- a/backend/class/field.php +++ b/backend/class/field.php @@ -1,420 +1,432 @@ normalizeField($field); - if(count($errors = app::getValidator('structure_config_field')->reset()->validate($field)) > 0) { - throw new \codename\core\exception(self::EXCEPTION_CONSTRUCT_CONFIGURATIONINVALID, \codename\core\exception::$ERRORLEVEL_FATAL, $errors); + if (count($errors = app::getValidator('structure_config_field')->reset()->validate($field)) > 0) { + throw new exception(self::EXCEPTION_CONSTRUCT_CONFIGURATIONINVALID, exception::$ERRORLEVEL_FATAL, $errors); } // Create config instance - $this->config = new \codename\core\config($field); - - if(!empty($this->config->get('field_elements'))) { - // Determine parse exceptions - // before output by smoke testing - try { - @eval('$ret = "' . $this->config->get('field_displayfield') . '";'); - } catch (\Throwable $e) { - if($e instanceof \ParseError) { - throw new exception('FIELD_DISPLAYFIELD_PARSE_ERROR', exception::$ERRORLEVEL_ERROR, $this->config->get('field_displayfield')); - } - } - } + $this->config = new config($field); return $this; } /** - * Generates the output for the form and returns it. - * @param bool $outputConfig [optional: do not render, but output config] - * @return string|array + * Sets defaults to the missing array keys + * @param array $field + * @return array + * @throws exception */ - public function output (bool $outputConfig = false) { - $data = $this->config->get(); - // @TODO: this may be used to check other array keys for callables, too. - // this NEEDS to check for string or function first, as we may have a value called 'Max' ... - if(!is_string($data['field_value']) && is_callable($data['field_value'])) { - // Replace field_value with its callable result - $data['field_value'] = $data['field_value'](); + protected function normalizeField(array $field): array + { + if (!array_key_exists('field_id', $field)) { + $field['field_id'] = str_replace(['[', ']'], '___', $field['field_name'] ?? ''); } - if(isset($data['field_elements']) && !is_string($data['field_elements']) && is_callable($data['field_elements'])) { - // Replace field_value with its callable result - $data['field_elements'] = $data['field_elements'](); + + if (!array_key_exists('field_fieldtype', $field) || strlen($field['field_fieldtype']) == 0) { + $field['field_fieldtype'] = 'input'; } - // re-structure field configuration for output - if(!empty($data['field_elements'])) { - $renderedElements = []; - foreach($data['field_elements'] as $element) { - $ret = null; - eval('$ret = "' . $data['field_displayfield'] . '";'); - - if(isset($data['field_idfield'])) { - $rendered = [ - 'id' => $element[$data['field_idfield']], - 'name' => $ret, - 'value' => $element[$data['field_valuefield']] - ]; - } else { - $rendered = [ - 'name' => $ret, - 'value' => $element[$data['field_valuefield']] - ]; - } - $renderedElements[] = $rendered; - } - $data['field_elements'] = $renderedElements; - $data['field_valuefield'] = 'value'; - $data['field_displayfield'] = 'name'; - if(isset($data['field_idfield'])) { - $data['field_idfield'] = 'id'; - } else { - - } + if (!array_key_exists('field_class', $field) || strlen($field['field_class']) == 0) { + $field['field_class'] = 'input'; } - // normalize field value at output time - // which may be the serialization as JSON - // $data['field_value'] = self::getNormalizedFieldValue($data['field_name'], $data['field_value'], $data['field_datatype']); + if (!array_key_exists('field_required', $field)) { + $field['field_required'] = false; + } - $data = self::normalizeFieldConfig($data); + if (!array_key_exists('field_readonly', $field)) { + $field['field_readonly'] = false; + } - if($outputConfig) { + if (!array_key_exists('field_ajax', $field)) { + $field['field_ajax'] = false; + } - // bare config - return $data; + if (!array_key_exists('field_noninput', $field)) { + $field['field_noninput'] = false; + } - } else { + if (!array_key_exists('field_placeholder', $field)) { + $field['field_placeholder'] = $field['field_title'] ?? ''; + } - // render - $templateEngine = $this->templateEngine; + if (!array_key_exists('field_value', $field)) { + $field['field_value'] = null; + } - // Fallback to default engine, if nothing set - if($this->templateEngine == null) { - $templateEngine = app::getTemplateEngine(); - } + if (!array_key_exists('field_datatype', $field)) { + $field['field_datatype'] = (($field['field_type'] ?? false) && $field['field_type'] == 'submit' ? null : 'text'); + } - return $templateEngine->render('field/' . $this->type . '/' . $this->config->get('field_type'), $data); + // + // if field_datatype is set (not null), perform field_value normalization + // + if ($field['field_datatype'] && ($field['field_name'] ?? false)) { + $field['field_value'] = self::getNormalizedFieldValue($field['field_name'], $field['field_value'], $field['field_datatype']); + // set type to relativetime by field_datatype is text_datetime_relative + // NOTE/WARNING: 2019-09-11: this is bad - do not change it to relativetime by default, as it overrides forced-hidden fields... + // this SHOULD be handled in crud and/or form (in this case possibly individually...) + // CHANGED: only change field_type to relativetime, if field is not already set as hidden... this may cause trouble, still + // if you want to use different field types. + // CHANGED 2020-05-26: Now it finally causes real trouble. Field Type cannot be changed, as this overrides it in the end (just before output) + // Moved type determination to crud + // if ($field['field_datatype'] === 'text_datetime_relative' && $field['field_type'] != 'hidden') { + // $field['field_type'] = 'relativetime'; + // } + } + if (!array_key_exists('field_validator', $field)) { + $field['field_validator'] = ''; } - } - /** - * Setter for the type of output to generate - * @param string $type - * @return field - * @todo REFACTOR OUT - */ - public function setType(string $type) : field { - $this->type = $type; - return $this; + if (!array_key_exists('field_description', $field)) { + $field['field_description'] = ''; + } + if (!array_key_exists('field_title', $field)) { + $field['field_title'] = ''; + } + return $field; } /** - * Defines which template engine to use - * @var \codename\core\templateengine - */ - protected $templateEngine = null; - - /** - * Setter for the templateEngine to use - * @param \codename\core\templateengine $templateEngine [description] - * @return field [description] + * [getNormalizedFieldValue description] + * @param string $fieldName [description] + * @param $value + * @param $datatype + * @return bool|int|mixed|null [type] [description] + * @throws exception */ - public function setTemplateEngine(\codename\core\templateengine $templateEngine) : field { - $this->templateEngine = $templateEngine; - return $this; - } + public static function getNormalizedFieldValue(string $fieldName, $value, $datatype): mixed + { + switch ($datatype) { + case 'boolean': + // pure boolean + if (is_bool($value)) { + // dont change. field_value has a valid datatype + break; + } + // int: 0 or 1 + if (is_int($value)) { + if ($value !== 1 && $value !== 0) { + throw new exception('EXCEPTION_FIELD_NORMALIZEFIELD_BOOLEAN_INVALID', exception::$ERRORLEVEL_ERROR, [ + 'field' => $fieldName, + 'value' => $value, + ]); + } + $value = $value === 1; + break; + } + // string boolean + if (is_string($value)) { + // fallback, empty string + if (strlen($value) === 0) { + $value = null; + break; + } + if ($value === '1') { + $value = true; + break; + } elseif ($value === '0') { + $value = false; + break; + } elseif ($value === 'true') { + $value = true; + break; + } elseif ($value === 'false') { + $value = false; + break; + } else { + throw new exception('EXCEPTION_FIELD_NORMALIZEFIELD_BOOLEAN_INVALID', exception::$ERRORLEVEL_ERROR, [ + 'field' => $fieldName, + 'value' => $value, + ]); + } + } + break; + case 'number_natural': + // string int prefilter: "null" and "" => null + if (is_string($value)) { + $value = $value === '' ? null : intval($value); + } + $value = $value === null ? null : intval($value); + } - /** - * [getTemplateEngine description] - * @return \codename\core\templateengine|null [description] - */ - public function getTemplateEngine() : ?\codename\core\templateengine { - return $this->templateEngine; + return $value; } /** * Returns the list of properties that can exist in a field instance * @return array */ - public static function getProperties() : array { + public static function getProperties(): array + { return self::$properties; } /** - * Returns the config instance of this field - * @return \codename\core\config + * Setter for the type of output to generate + * @param string $type + * @return field + * @todo REFACTOR OUT */ - public function getConfig() : \codename\core\config { - return $this->config; + public function setType(string $type): field + { + $this->type = $type; + return $this; } /** * Returns true if the field is required * @return bool */ - public function isRequired() : bool { + public function isRequired(): bool + { return $this->config->get('field_required') ?? false; } /** - * Sets defaults to the missing array keys - * @param array $field - * @return array + * Sets a property in the instance's configuration object + * @param string $property + * @param $value + * @return static */ - protected function normalizeField(array $field) : array { - if(!array_key_exists('field_id', $field)) { - $field['field_id'] = str_replace(array('[', ']'), '___', $field['field_name']); - } - - if(!array_key_exists('field_fieldtype', $field) || strlen($field['field_fieldtype']) == 0) { - $field['field_fieldtype'] = 'input'; - } - - if(!array_key_exists('field_class', $field) || strlen($field['field_class']) == 0) { - $field['field_class'] = 'input'; - } + public function setProperty(string $property, $value): static + { + $cfg = $this->config->get(); + $cfg[$property] = $value; + $this->config = new config($cfg); + return $this; + } - if(!array_key_exists('field_required', $field)) { - $field['field_required'] = false; - } + /** + * overwrites the field_value + * by creating a fresh internal field config + * @param mixed $value + */ + public function setValue(mixed $value): void + { + $cfg = $this->config->get(); + $cfg['field_value'] = $value; + $this->config = new config($cfg); + } - if(!array_key_exists('field_readonly', $field)) { - $field['field_readonly'] = false; - } + /** + * {@inheritDoc} + * custom serialization to allow bare config field output + * @return mixed + * @throws ReflectionException + * @throws exception + */ + public function jsonSerialize(): mixed + { + return $this->output(true); + } - if(!array_key_exists('field_ajax', $field)) { - $field['field_ajax'] = false; + /** + * Generates the output for the form and returns it. + * @param bool $outputConfig [optional: do not render, but output config] + * @return string|array + * @throws ReflectionException + * @throws exception + */ + public function output(bool $outputConfig = false): array|string + { + $data = $this->config->get(); + // @TODO: this may be used to check other array keys for callables, too. + // this NEEDS to check for string or function first, as we may have a value called 'Max' ... + if (!is_string($data['field_value']) && is_callable($data['field_value'])) { + // Replace field_value with its callable result + $data['field_value'] = $data['field_value'](); } - - if(!array_key_exists('field_noninput', $field)) { - $field['field_noninput'] = false; + if (isset($data['field_elements']) && !is_string($data['field_elements']) && is_callable($data['field_elements'])) { + // Replace field_value with its callable result + $data['field_elements'] = $data['field_elements'](); } - if(!array_key_exists('field_placeholder', $field)) { - $field['field_placeholder'] = $field['field_title'] ?? ''; + // re-structure field configuration for output + if (!empty($data['field_elements']) && is_array($data['field_elements'])) { + $renderedElements = []; + foreach ($data['field_elements'] as $element) { + $ret = null; + eval('$ret = "' . $data['field_displayfield'] . '";'); + + if (isset($data['field_idfield'])) { + $rendered = [ + 'id' => $element[$data['field_idfield']], + 'name' => $ret, + 'value' => $element[$data['field_valuefield']], + ]; + } else { + $rendered = [ + 'name' => $ret, + 'value' => $element[$data['field_valuefield']], + ]; + } + + // option support + if ($data['field_optionfield'] ?? false) { + $ret = null; + eval('$ret = "' . $data['field_optionfield'] . '";'); + $rendered['option'] = $ret; + } + + $renderedElements[] = $rendered; + } + $data['field_elements'] = $renderedElements; + $data['field_valuefield'] = 'value'; + $data['field_displayfield'] = 'name'; + if (isset($data['field_idfield'])) { + $data['field_idfield'] = 'id'; + } + if (isset($data['field_optionfield'])) { + $data['field_optionfield'] = 'option'; + } } - if(!array_key_exists('field_value', $field)) { - $field['field_value'] = null; - } + // normalize field value at output time + // which may be the serialization as JSON + // $data['field_value'] = self::getNormalizedFieldValue($data['field_name'], $data['field_value'], $data['field_datatype']); - if(!array_key_exists('field_datatype', $field)) { - $field['field_datatype'] = ($field['field_type'] == 'submit' ? null : 'text'); - } + $data = self::normalizeFieldConfig($data); - // - // if field_datatype is set (not null), perform field_value normalization - // - if($field['field_datatype'] && ($field['field_name'] ?? false)) { - $field['field_value'] = self::getNormalizedFieldValue($field['field_name'], $field['field_value'], $field['field_datatype']); - // set type to relativetime by field_datatype is text_datetime_relative - // NOTE/WARNING: 2019-09-11: this is bad - do not change it to relativetime by default, as it overrides forced-hidden fields... - // this SHOULD be handled in crud and/or form (in this case possibly individually...) - // CHANGED: only change field_type to relativetime, if field is not already set as hidden... this may cause trouble, still - // if you want to use different field types. - // CHANGED 2020-05-26: Now it finally causes real trouble. Field Type cannot be changed, as this overrides it in the end (just before output) - // Moved type determination to crud - // if ($field['field_datatype'] === 'text_datetime_relative' && $field['field_type'] != 'hidden') { - // $field['field_type'] = 'relativetime'; - // } - } + if ($outputConfig) { + // bare config + return $data; + } else { + // render + $templateEngine = $this->templateEngine; - if(!array_key_exists('field_validator', $field)) { - $field['field_validator'] = ''; - } + // Fallback to default engine, if nothing set + if ($this->templateEngine == null) { + $templateEngine = app::getTemplateEngine(); + } - if(!array_key_exists('field_description', $field)) { - $field['field_description'] = ''; - } - if(!array_key_exists('field_title', $field)) { - $field['field_title'] = ''; + return $templateEngine->render('field/' . $this->type . '/' . $this->config->get('field_type'), $data); } - return $field; } /** * [normalizeFieldConfig description] - * @param array $fielddata [description] + * @param array $fielddata [description] * @return array + * @throws exception */ - protected static function normalizeFieldConfig(array $fielddata) : array { - if($fielddata['field_type'] == 'form') { - if(($fielddata['form'] ?? false) && ($fielddata['form'] instanceof \codename\core\ui\form)) { - if(is_array($fielddata['field_value'])) { - foreach($fielddata['form']->getFields() as $fieldInstance) { - $fieldName = $fieldInstance->getProperty('field_name'); - $fieldDatatype = $fieldInstance->getProperty('field_datatype'); - $fieldValue = $fieldInstance->getProperty('field_value'); - $fielddata['field_value'][$fieldName] = self::getNormalizedFieldValue($fieldName, $fieldValue, $fieldDatatype); + protected static function normalizeFieldConfig(array $fielddata): array + { + if ($fielddata['field_type'] == 'form') { + if (($fielddata['form'] ?? false) && ($fielddata['form'] instanceof form)) { + if (is_array($fielddata['field_value'])) { + foreach ($fielddata['form']->getFields() as $fieldInstance) { + $fieldName = $fieldInstance->getConfig()->get('field_name'); + $fieldDatatype = $fieldInstance->getConfig()->get('field_datatype'); + $fieldValue = $fieldInstance->getConfig()->get('field_value'); + $fielddata['field_value'][$fieldName] = self::getNormalizedFieldValue($fieldName, $fieldValue, $fieldDatatype); + } + } } - } + } else { + $fielddata['field_value'] = self::getNormalizedFieldValue($fielddata['field_name'], $fielddata['field_value'], $fielddata['field_datatype']); } - } else { - $fielddata['field_value'] = self::getNormalizedFieldValue($fielddata['field_name'], $fielddata['field_value'], $fielddata['field_datatype']); - } - return $fielddata; + return $fielddata; } /** - * [getNormalizedFieldValue description] - * @param string $fieldName [description] - * @param [type] $value [description] - * @param [type] $datatype [description] - * @return [type] [description] + * Returns the config instance of this field + * @return config */ - public static function getNormalizedFieldValue(string $fieldName, $value, $datatype) { - switch($datatype) { - case 'boolean': - // pure boolean - if(is_bool($value)) { - // dont change. field_value has a valid datatype - break; - } - // int: 0 or 1 - if(is_int($value)) { - if($value !== 1 && $value !== 0) { - throw new exception('EXCEPTION_FIELD_NORMALIZEFIELD_BOOLEAN_INVALID', exception::$ERRORLEVEL_ERROR, [ - 'field' => $fieldName, - 'value' => $value - ]); - } - $value = $value === 1 ? true : false; - break; - } - // string boolean - if(is_string($value)) { - // fallback, empty string - if(strlen($value) === 0) { - $value = null; - break; - } - if($value === '1') { - $value = true; - break; - } else if($value === '0') { - $value = false; - break; - } else if($value === 'true') { - $value = true; - break; - } elseif ($value === 'false') { - $value = false; - break; - } else { - throw new exception('EXCEPTION_FIELD_NORMALIZEFIELD_BOOLEAN_INVALID', exception::$ERRORLEVEL_ERROR, [ - 'field' => $fieldName, - 'value' => $value - ]); - } - } - case 'number_natural': - // string int prefilter: "null" and "" => null - if(is_string($value)) { - $value = $value === '' ? null : intval($value); - } - $value = $value === null ? null : intval($value); - } - - // DEBUG: - // \codename\core\app::getResponse()->setData('meh debug_'.$fieldName, [ - // 'value' => $value, - // 'datatype' => $datatype - // ]); - // - return $value; + public function getConfig(): config + { + return $this->config; } /** - * Returns a property from the instance's configuration object - * @param string $property - * @return mixed - * @deprecated + * [getTemplateEngine description] + * @return templateengine|null [description] */ - public function getProperty(string $property) { - return $this->config->get($property); + public function getTemplateEngine(): ?templateengine + { + return $this->templateEngine; } /** - * Sets a property in the instance's configuration object - * @param string $property - * @return mixed - * @deprecated + * Setter for the templateEngine to use + * @param templateengine $templateEngine [description] + * @return field [description] */ - public function setProperty(string $property, $value) { - $cfg = $this->config->get(); - $cfg[$property] = $value; - $this->config = new \codename\core\config($cfg); + public function setTemplateEngine(templateengine $templateEngine): field + { + $this->templateEngine = $templateEngine; return $this; } /** - * overwrites the field_value - * by creating a fresh internal field config - * @param mixed $value - */ - public function setValue($value) { - $cfg = $this->config->get(); - $cfg['field_value'] = $value; - $this->config = new \codename\core\config($cfg); - return; - } - - /** - * @inheritDoc - * custom serialization to allow bare config field output + * Returns a property from the instance's configuration object + * @param string $property + * @return mixed */ - public function jsonSerialize() + public function getProperty(string $property): mixed { - return $this->output(true); + return $this->config->get($property); } } diff --git a/backend/class/fieldset.php b/backend/class/fieldset.php index 43f6f59..7b56b4b 100644 --- a/backend/class/fieldset.php +++ b/backend/class/fieldset.php @@ -1,146 +1,161 @@ Override these templates by adding these files in your application's directory + * It uses several frontend resources located in the core frontend folder. + * Override these templates by adding these files in your application's directory * @package core * @since 2016-03-17 */ -class fieldset implements \JsonSerializable { - +class fieldset implements JsonSerializable +{ + /** + * Defines which template engine to use + * @var null|templateengine + */ + protected ?templateengine $templateEngine = null; /** * Contains the display type of the instance * @var string $type */ - private $type = 'default'; - + private string $type = 'default'; /** * Contains all the data for the form element * @var array $data * @todo make instance of datacontainer? */ - private $data = array(); + private array $data; /** * Creates a fieldset. A fieldset belongs into a form and contains multiple instances of \codename\core\ui\field * @param array $fieldset - * @return fieldset + * @throws ReflectionException + * @throws exception */ - public function __CONSTRUCT(array $fieldset) { - $fieldset['fieldset_id'] = $fieldset['fieldset_id'] ?? $fieldset['fieldset_name']; - if(isset($fieldset['fieldset_name_override'])) { - $fieldset['fieldset_name'] = $fieldset['fieldset_name_override']; + public function __construct(array $fieldset) + { + $fieldset['fieldset_id'] = $fieldset['fieldset_id'] ?? $fieldset['fieldset_name'] ?? null; + if (isset($fieldset['fieldset_name_override'])) { + $fieldset['fieldset_name'] = $fieldset['fieldset_name_override']; } else { - $fieldset['fieldset_name'] = app::getTranslate()->translate('DATAFIELD.FIELDSET_' . $fieldset['fieldset_name']); + $fieldset['fieldset_name'] = app::getTranslate()->translate('DATAFIELD.FIELDSET_' . $fieldset['fieldset_name']); } $this->data = $fieldset; - if(!array_key_exists('fields', $this->getData())) { - $this->data['fields'] = array(); + if (!array_key_exists('fields', $this->getData())) { + $this->data['fields'] = []; } } /** - * Adds a field to the data array of this instance - * @param field $field - * @param int $position [position where to insert the field; -1 is last, -2 the second last] - * @return fieldset - */ - public function addField(field $field, int $position = -1) : fieldset { - if($position !== -1) { - array_splice($this->data['fields'], $position, 0, [ $field ]); - return $this; - } else { - array_push($this->data['fields'], $field); - return $this; - } - } - - /** - * Will output the fieldset's content - * @param bool $outputConfig [optional: do not render, but output config] - * @return string|array + * Returns the instance's data array + * @return array */ - public function output(bool $outputConfig = false) { - if($outputConfig) { - // just data for pure-data output (e.g. serializer) + protected function getData(): array + { return $this->data; - } else { - $templateEngine = $this->templateEngine; - if($templateEngine == null) { - $templateEngine = app::getTemplateEngine(); - } - - // override field template engines on a weak basis - foreach($this->getFields() as $field) { - if($field->getTemplateEngine() == null) { - $field->setTemplateEngine($templateEngine); - } - } - - return $templateEngine->render('fieldset/' . $this->type, $this->getData()); - } } /** - * Returns the instance's data array - * @return array + * Adds a field to the data array of this instance + * @param field $field + * @param int $position [position where to insert the field; -1 is last, -2 the second last] + * @return fieldset */ - protected function getData() : array { - return $this->data; + public function addField(field $field, int $position = -1): fieldset + { + if ($position !== -1) { + array_splice($this->data['fields'], $position, 0, [$field]); + } else { + $this->data['fields'][] = $field; + } + return $this; } /** * Setter for the type of output to generate - * @author Kevin Dargel * @param string $type * @return fieldset */ - public function setType(string $type) : fieldset { + public function setType(string $type): fieldset + { $this->type = $type; return $this; } /** - * Defines which template engine to use - * @var \codename\core\templateengine + * {@inheritDoc} + * custom serialization to allow bare config field output + * @return mixed + * @throws ReflectionException + * @throws exception */ - protected $templateEngine = null; + public function jsonSerialize(): mixed + { + return $this->output(true); + } /** - * Setter for the templateEngine to use - * @param \codename\core\templateengine $templateEngine [description] - * @return fieldset [description] + * Will output the fieldset's content + * @param bool $outputConfig [optional: do not render, but output config] + * @return string|array + * @throws ReflectionException + * @throws exception */ - public function setTemplateEngine(\codename\core\templateengine $templateEngine) : fieldset { - $this->templateEngine = $templateEngine; - return $this; + public function output(bool $outputConfig = false): string|array + { + if ($outputConfig) { + // just data for pure-data output (e.g., serializer) + return $this->data; + } else { + $templateEngine = $this->templateEngine; + if ($templateEngine == null) { + $templateEngine = app::getTemplateEngine(); + } + + // override field template engines on a weak basis + foreach ($this->getFields() as $field) { + if ($field->getTemplateEngine() == null) { + $field->setTemplateEngine($templateEngine); + } + } + + return $templateEngine->render('fieldset/' . $this->type, $this->getData()); + } } /** * [getTemplateEngine description] - * @return \codename\core\templateengine|null [description] + * @return templateengine|null [description] */ - public function getTemplateEngine() : ?\codename\core\templateengine { - return $this->templateEngine; + public function getTemplateEngine(): ?templateengine + { + return $this->templateEngine; } /** - * @return \codename\core\ui\field[] + * Setter for the templateEngine to use + * @param templateengine $templateEngine [description] + * @return fieldset [description] */ - public function getFields() : array { - return $this->data['fields']; + public function setTemplateEngine(templateengine $templateEngine): fieldset + { + $this->templateEngine = $templateEngine; + return $this; } /** - * @inheritDoc - * custom serialization to allow bare config field output + * @return field[] */ - public function jsonSerialize() + public function getFields(): array { - return $this->output(true); + return $this->data['fields']; } - } diff --git a/backend/class/form.php b/backend/class/form.php index d7c2be2..3f23ca1 100644 --- a/backend/class/form.php +++ b/backend/class/form.php @@ -1,7 +1,21 @@ e.g. Use it to output the form in a standard view + * E.g., Use it to output the form in a standard view * @var string */ - CONST CALLBACK_FORM_NOT_SENT = 'FORM_NOT_SENT'; + public const string CALLBACK_FORM_NOT_SENT = 'FORM_NOT_SENT'; /** - * This callback is used when the form cannot be validated correctly - *
e.g. use it to display a standard error output for all forms. + * This callback is used when the form cannot be validated correctly, + * e.g., use it to display a standard error output for all forms. * @var string */ - CONST CALLBACK_FORM_NOT_VALID = 'FORM_NOT_VALID'; + public const string CALLBACK_FORM_NOT_VALID = 'FORM_NOT_VALID'; /** - * This is the callback that runs, when your form is valid. - *
e.g. use it to store the information by default. + * This is the callback that runs when your form is valid. + * E.g., use it to store the information by default. * @var string */ - CONST CALLBACK_FORM_VALID = 'FORM_VALID'; + public const string CALLBACK_FORM_VALID = 'FORM_VALID'; /** * This is the callback that runs during/after validation (before finishing it) - *
e.g. to hook into the validation process and run some more validators + * e.g., to hook into the validation process and run some more validators * @var string */ - CONST CALLBACK_FORM_VALIDATION = 'FORM_VALIDATION'; - - + public const string CALLBACK_FORM_VALIDATION = 'FORM_VALIDATION'; + /** + * [EXCEPTION_FORM_VALIDATION_SELECTIVE_UNKNOWN_FIELD_IDS description] + * @var string + */ + public const string EXCEPTION_FORM_VALIDATION_SELECTIVE_UNKNOWN_FIELD_IDS = "EXCEPTION_FORM_VALIDATION_SELECTIVE_UNKNOWN_FIELD_IDS"; /** * Contains the configuration for the form * @var array $config - * @todo implement \codename\core\config */ - public $config = array(); - + public array $config = []; /** * Contains the form fields for the form - * @var \codename\core\ui\field[] $fields - */ - public $fields = array(); - - /** - * Contains an array of data (fieldnames as keys, their sent value as value) - * @todo make this an instance of (idea) \codename\core\data - * @var \codename\core\datacontainer - */ - protected $data = null; - - /** - * Defines what form and field objects shall be used when generating output - * @var string + * @var field[] $fields */ - protected $type = 'default'; - + public array $fields = []; /** * Contains all the fieldsets that will be displayed in the CRUD generator - * @var \codename\core\ui\fieldset[] + * @var fieldset[] */ - public $fieldsets = array(); - + public array $fieldsets = []; /** * Contains the errorstack for this form - * @var \codename\core\errorstack + * @var errorstack */ - public $errorstack = null; - + public errorstack $errorstack; /** * determines the output type * either false (rendered) or true (pure config) * @var bool */ - public $outputConfig = false; - + public bool $outputConfig = false; + /** + * Contains an array of data (fieldnames as keys, their send value as value) + * @var datacontainer + */ + protected datacontainer $data; + /** + * Defines what form and field objects shall be used when generating output + * @var string + */ + protected string $type = 'default'; /** * Contains a list of callbacks * @example form->addCallback(\codename\core\ui\form::CALLBACK_FORM_NOT_SENT, function($form) {}); * @example form->addCallback(\codename\core\ui\form::CALLBACK_FORM_NOT_VALID, function($form) {}); * @var array */ - protected $callbacks = array(); + protected array $callbacks = []; + /** + * [protected description] + * @var null|field + */ + protected ?field $formSentField = null; + /** + * [protected description] + * @var null|datacontainer + */ + protected ?datacontainer $requestData = null; + /** + * Defines which template engine to use + * @var null|templateengine + */ + protected ?templateengine $templateEngine = null; /** * Stores the configuration and fields in the instance * @param array $data [config] - * @return form + * @throws ReflectionException + * @throws exception */ - public function __CONSTRUCT(array $data) { + public function __construct(array $data) + { if (count($errors = app::getValidator('structure_config_form')->reset()->validate($data)) > 0) { - throw new \codename\core\exception(self::EXCEPTION_CONSTRUCT_CONFIGURATIONINVALID, \codename\core\exception::$ERRORLEVEL_FATAL, $errors); + throw new exception(self::EXCEPTION_CONSTRUCT_CONFIGURATIONINVALID, exception::$ERRORLEVEL_FATAL, $errors); } - $this->data = new \codename\core\datacontainer(array()); + $this->data = new datacontainer([]); $this->config = $data; - $this->errorstack = new \codename\core\errorstack("VALIDATION"); + $this->errorstack = new errorstack("VALIDATION"); - // if(!isset($this->config['form_text_requiredfields'])) { - // $this->config['form_text_requiredfields'] = app::translate('CRUD.REQUIREDFIELDS'); - // } - - $this->addCallback(\codename\core\ui\form::CALLBACK_FORM_NOT_SENT, function(\codename\core\ui\form $form) { + $this->addCallback(form::CALLBACK_FORM_NOT_SENT, function (form $form) { app::getResponse()->setData('form', $form->output($this->outputConfig)); - return; - })->addCallback(\codename\core\ui\form::CALLBACK_FORM_NOT_VALID, function(\codename\core\ui\form $form) { + })->addCallback(form::CALLBACK_FORM_NOT_VALID, function (form $form) { app::getResponse()->setData('errors', $form->getErrorstack()->getErrors()); app::getResponse()->setData('context', 'form'); app::getResponse()->setData('view', 'error'); - return; }); return $this; } /** - * [protected description] - * @var field + * I will overwrite the $callback for the given $identifier. + * Please add Callbacks by requiring an instance of \codename\core\ui\form named $form. + * @param string $identifier + * @param callable $callback + * @return form + * @example ->addCallback(\codename\core\ui\form::CALLBACK_FORM_NOT_SENT, function(\codename\core\ui\form $form) {die('OK!');}); */ - protected $formSentField = null; + public function addCallback(string $identifier, callable $callback): form + { + $this->callbacks[$identifier] = $callback; + return $this; + } /** * Outputs the form HTML code - * @param bool $outputConfig [optional: do not render, but output config] - * @return string|array + * @param bool $outputConfig [optional: do not render, but output config] + * @return string|form + * @throws ReflectionException + * @throws exception */ - public function output(bool $outputConfig = false) { - if(count($this->fields) == 0 && count($this->fieldsets) == 0) { - throw new \codename\core\exception(self::EXCEPTION_OUTPUT_FORMISEMPTY, \codename\core\exception::$ERRORLEVEL_FATAL, null); + public function output(bool $outputConfig = false): string|form + { + if (count($this->fields) == 0 && count($this->fieldsets) == 0) { + throw new exception(self::EXCEPTION_OUTPUT_FORMISEMPTY, exception::$ERRORLEVEL_FATAL, null); } - if($this->formSentField == null) { - $this->addField($this->formSentField = new field(array( - 'field_name' => 'formSent' . $this->config['form_id'], - 'field_type' => 'hidden', - 'field_value' => 1 - ))); + if ($this->formSentField == null) { + $this->addField( + $this->formSentField = new field([ + 'field_name' => 'formSent' . $this->config['form_id'], + 'field_type' => 'hidden', + 'field_value' => 1, + ]) + ); } - if($outputConfig) { - return $this; + if ($outputConfig) { + return $this; } else { - $templateEngine = $this->templateEngine; - if($templateEngine == null) { - $templateEngine = app::getTemplateEngine(); - } - - // override field template engines on a weak basis - foreach($this->fields as $field) { - if($field->getTemplateEngine() == null) { - $field->setTemplateEngine($templateEngine); + $templateEngine = $this->templateEngine; + if ($templateEngine == null) { + $templateEngine = app::getTemplateEngine(); } - } - - // override fieldset template engines on a weak basis - foreach($this->fieldsets as $fieldset) { - if($fieldset->getTemplateEngine() == null) { - $fieldset->setTemplateEngine($templateEngine); - } - } - - return $templateEngine->render('form/' . $this->type . '/form', $this); - } - } - - /** - * Returns true if the form of the instance has been sent in a previous request. - *
This is determined by checking if the request instance contains the "formSent.$FORMID" field - * @return bool - */ - public function isSent() : bool { - return app::getInstance('request')->isDefined('formSent' . $this->config['form_id']); - } - - /** - * Returns true if all fields of the form have been filled correctly. - *
Checks if required fields are filled - *
Also uses the given field_types as validators to check the fields - * @return bool - */ - public function isValid(array $fieldIds = null) : bool { - - $tFields = $this->fields; - - foreach($this->fieldsets as $fieldset) { - $tFields = array_merge($tFields, $fieldset->getFields()); - } - foreach($tFields as $field) { - - // skip noninput fields - if($field->getConfig()->get('field_noninput') === true) { - continue; - } - - // skip internal sent-determination field - if($field === $this->formSentField) { - continue; - } - - if($fieldIds !== null && is_array($fieldIds)) { - if(sizeof($fieldIds) > 0) { - $currentFieldId = $field->getConfig()->get('field_id'); - if(in_array($currentFieldId, $fieldIds)) { - $index = array_search($currentFieldId, $fieldIds); - if($index !== FALSE) { - unset($fieldIds[$index]); // Remove it from the to-be-checked IDs + // override field template engines on a weak basis + foreach ($this->fields as $field) { + if ($field->getTemplateEngine() == null) { + $field->setTemplateEngine($templateEngine); } - } else { - continue; - } - } else { - break; - } - } else { - if($field->getConfig()->get('field_ajax') === true) { - continue; - } - } - - - - $fieldname = $field->getConfig()->get('field_name'); - - // Check for existance of the field - // and its value (to be not-null and not an empty string (?)) - if($field->isRequired() && (!$this->fieldSent($field) || empty($this->fieldValue($field))) ) { - $this->errorstack->addError($fieldname, 'FIELD_NOT_SET'); - continue; - } - - $fieldtype = $field->getConfig()->get('field_datatype'); - - $displaytype = $field->getConfig()->get('field_type'); - if($displaytype == 'checkbox') { - $fieldtype = 'boolean'; - } - if(is_null($fieldtype)) { - continue; - } - - if($displaytype == 'form') { - $subform = $field->getProperty('form'); - - // provide subform-related data to the subform - if($subform instanceof \codename\core\ui\form && $this->fieldValue($field) !== null) { - $subform->getErrorstack()->reset(); - $subform->setFormRequest($this->fieldValue($field)); - if(!$subform->isValid()) { - $this->errorstack->addError($fieldname, 'FIELD_INVALID', $subform->getErrorstack()->getErrors()); - } } - continue; - } - if(($value = $this->fieldValue($field)) != null) { - $validation = app::getValidator($fieldtype)->reset()->validate($value); - if(count($validation) > 0) { - $this->errorstack->addError($fieldname, 'FIELD_INVALID', $validation); + // override fieldset template engines on a weak basis + foreach ($this->fieldsets as $fieldset) { + if ($fieldset->getTemplateEngine() == null) { + $fieldset->setTemplateEngine($templateEngine); + } } - } - } - - if($fieldIds !== NULL && is_array($fieldIds) && sizeof($fieldIds) > 0) { - // some field ids do not exist - $fieldIds has to be of size zero here. - throw new \codename\core\exception(self::EXCEPTION_FORM_VALIDATION_SELECTIVE_UNKNOWN_FIELD_IDS, \codename\core\exception::$ERRORLEVEL_ERROR, $fieldIds); - } - - // FIRE ON VALIDATION HOOK - $this->fireCallback(\codename\core\ui\form::CALLBACK_FORM_VALIDATION); - - return $this->errorstack->isSuccess(); - } - /** - * [EXCEPTION_FORM_VALIDATION_SELECTIVE_UNKNOWN_FIELD_IDS description] - * @var string - */ - const EXCEPTION_FORM_VALIDATION_SELECTIVE_UNKNOWN_FIELD_IDS = "EXCEPTION_FORM_VALIDATION_SELECTIVE_UNKNOWN_FIELD_IDS"; - - /** - * Returns true if the given $field has been submitted in the las request - *
Uses the request object to find the fields's name - * @param \codename\core\ui\field $field - * @return bool - */ - public function fieldSent(\codename\core\ui\field $field) : bool { - switch ($field->getProperty('field_type')) { - case 'file' : - case 'signature' : // temporary fix for signature fields - if(app::getRequest() instanceof \codename\core\request\filesInterface) { - return array_key_exists($field->getProperty('field_name'), app::getRequest()->getFiles()); - } else { - return array_key_exists($field->getProperty('field_name'), $_FILES); - } - break; - default: - if($this->getFormRequest()->isDefined($field->getProperty('field_name'))) { - if(is_array($value = $this->getFormRequest()->getData($field->getProperty('field_name'))) && count($value) === 0) { - return false; - } else { - return true; - } - } else { - return false; - } - // $request = $this->getFormRequest()->getData($field->getProperty('field_name')); - // if(is_array($request)) { - // return sizeof($request) > 0; - // } else { - // return (strlen($request) > 0); - // } - break; + return $templateEngine->render('form/' . $this->type . '/form', $this); } - return false; } /** - * [protected description] - * @var \codename\core\datacontainer - */ - protected $requestData = null; - - /** - * [getFormRequest description] - * @return \codename\core\request [description] + * Adds the given $field to the form instance + * @param field $field + * @param int $position [position where to insert the field; -1 is last, -2 the second last] + * @return form */ - protected function getFormRequest() : \codename\core\datacontainer { - if($this->requestData == null) { - $this->setFormRequest(app::getRequest()->getData()); - } - return $this->requestData; + public function addField(field $field, int $position = -1): form + { + if ($position !== -1) { + array_splice($this->fields, $position, 0, [$field]); + } else { + $this->fields[] = $field; + } + return $this; } /** - * [setFormRequest description] - * @param array $requestData [description] + * [getTemplateEngine description] + * @return null|templateengine [description] */ - public function setFormRequest(array $requestData) { - $this->requestData = new \codename\core\datacontainer( $requestData ); + public function getTemplateEngine(): ?templateengine + { + return $this->templateEngine; } /** - * Returns the given $field instance's value depending on it's datatype - * @param \codename\core\ui\field $field [description] - * @return mixed [description] + * Setter for the templateEngine to use + * @param templateengine $templateEngine [description] + * @return form [description] */ - public function fieldValue(\codename\core\ui\field $field) { - switch ($field->getProperty('field_type')) { - case 'checkbox' : - return $this->getFormRequest()->isDefined($field->getProperty('field_name')); - break; - case 'file' : - return $_FILES[$field->getProperty('field_name')] ?? null; - break; - default: - return $this->getFormRequest()->getData($field->getProperty('field_name')); - break; - } - return null; + public function setTemplateEngine(templateengine $templateEngine): form + { + $this->templateEngine = $templateEngine; + return $this; } /** * Returns the errorstack instance of this form - * @return \codename\core\errorstack + * @return errorstack */ - public function getErrorstack() : \codename\core\errorstack { + public function getErrorstack(): errorstack + { return $this->errorstack; } - /** - * Returns the data stored in the form - *
Will return the whole dataset if you don't supply a $key - *
Will return null if the given $key does not exist in the dataset - * @return multitype - */ - public function getData(string $key = '') { - if(is_null($this->data)) { - foreach($this->fields as $field) { - $this->data->setData($field->getConfig()->get('field_name'), $this->fieldValue($field)); - } - } - $data = $this->getFormRequest()->getData(); - if(isset($_FILES)) { - if(app::getRequest() instanceof \codename\core\request\filesInterface) { - $data = array_merge($data, app::getRequest()->getFiles()); - } else { - $data = array_merge($data, $_FILES); - } - } - $this->data->addData($data); - return $this->data->getData($key); - } - /** * normalizes given data * using the form fields in this form * - * @param array $data [description] - * @return array - */ - public function normalizeData(array $data) : ?array { - $newdata = []; - foreach($this->fields as $field) { - $key = $field->getProperty('field_name'); - if(array_key_exists($key, $data)) { - if($data[$key] && $field->getProperty('field_datatype') === 'structure' && $elementDatatype = $field->getProperty('field_element_datatype')) { - // if not an array, make it an array. - if(!is_array($data[$key])) { - $data[$key] = [ $data[$key] ]; + * @param array $data [description] + * @return null|array + * @throws exception + */ + public function normalizeData(array $data): ?array + { + $newdata = []; + foreach ($this->fields as $field) { + $key = $field->getConfig()->get('field_name'); + if (array_key_exists($key, $data)) { + if ($data[$key] && $field->getConfig()->get('field_datatype') === 'structure' && $elementDatatype = $field->getConfig()->get('field_element_datatype')) { + // if not an array, make it an array. + if (!is_array($data[$key])) { + $data[$key] = [$data[$key]]; + } + $normalizedValue = array_map(function ($element) use ($key, $elementDatatype) { + return field::getNormalizedFieldValue($key, $element, $elementDatatype); + }, $data[$key]); + $newdata[$key] = $normalizedValue; + } else { + $newdata[$key] = field::getNormalizedFieldValue($key, $data[$key] ?? null, $field->getConfig()->get('field_datatype')); + } } - $normalizedValue = array_map(function($element) use( $key, $elementDatatype) { - return \codename\core\ui\field::getNormalizedFieldValue($key, $element, $elementDatatype); - }, $data[$key]); - $newdata[$key] = $normalizedValue; - } else { - $newdata[$key] = \codename\core\ui\field::getNormalizedFieldValue($key, $data[$key] ?? null, $field->getProperty('field_datatype')); - } } - } - return count($newdata) > 0 ? $newdata : null; + return count($newdata) > 0 ? $newdata : null; } - /** * Setter for the type of output to generate * @param string $type * @return form */ - public function setType(string $type) : form { + public function setType(string $type): form + { $this->type = $type; return $this; } /** - * Defines which template engine to use - * @var \codename\core\templateengine + * Sets the identifier for the current form + * @param string $identifier + * @return form */ - protected $templateEngine = null; + public function setId(string $identifier): form + { + $this->config['form_id'] = "core_form_" . $identifier; + return $this; + } /** - * Setter for the templateEngine to use - * @param \codename\core\templateengine $templateEngine [description] - * @return form [description] + * sets the form action value in the config + * @param string $value [description] + * @return form [description] */ - public function setTemplateEngine(\codename\core\templateengine $templateEngine) : form { - $this->templateEngine = $templateEngine; - return $this; + public function setAction(string $value): form + { + $this->config['form_action'] = $value; + return $this; } /** - * [getTemplateEngine description] - * @return \codename\core\templateengine\null [description] + * Adds a $fieldset to the instance + * @param fieldset $fieldset + * @param int $position [position where to insert the fieldset; -1 is last, -2 the second last] + * @return form */ - public function getTemplateEngine() : ?\codename\core\templateengine { - return $this->templateEngine; + public function addFieldset(fieldset $fieldset, int $position = -1): form + { + if ($position !== -1) { + array_splice($this->fieldsets, $position, 0, [$fieldset]); + } else { + $this->fieldsets[] = $fieldset; + } + return $this; } /** - * Adds the given $field to the form instance - * @param field $field - * @param int $position [position where to insert the field; -1 is last, -2 the second last] + * I am a standardized method for working off an existing form instance. + * By default, the FORM_NOT_SENT callback will use the form template to output it. + * By default, the FORM_NOT_VALID callback will output the occurred errors using the standard outputs. * @return form + * @throws ReflectionException + * @throws exception */ - public function addField(field $field, int $position = -1) : form { - if($position !== -1) { - array_splice($this->fields, $position, 0, [ $field ]); - return $this; - } else { - array_push($this->fields, $field); + public function work(): form + { + if (!$this->isSent()) { + $this->fireCallback(form::CALLBACK_FORM_NOT_SENT); + return $this; + } + + if (!$this->isValid()) { + $this->fireCallback(form::CALLBACK_FORM_NOT_VALID); + return $this; + } + + $this->fireCallback(form::CALLBACK_FORM_VALID); + return $this; - } } /** - * Returns all the fields in the form instance - * @return field[] + * Returns true if the form of the instance has been sent in a previous request. + * This is determined by checking if the request instance contains the "formSent.$FORMID" field + * @return bool + * @throws ReflectionException + * @throws exception */ - public function getFields() : array { - return $this->fields; + public function isSent(): bool + { + return app::getInstance('request')->isDefined('formSent' . $this->config['form_id']); } /** - * Sets the identifier for the current form + * I will try accessing the callback identified by the given $identifier. + * I will pass the current instance of \codename\core\ui\form to the callback method named $form + * If the desired callback does not exist, I will do nothing. * @param string $identifier - * @return form + * @return void */ - public function setId(string $identifier) : form { - $this->config['form_id'] = "core_form_" . $identifier; - return $this; + private function fireCallback(string $identifier): void + { + if (array_key_exists($identifier, $this->callbacks)) { + call_user_func($this->callbacks[$identifier], $this); + } } /** - * sets the form action value in the config - * @param string $value [description] - * @return form [description] + * Returns true if all fields of the form have been filled correctly. + * Checks if required fields are filled + * Also use the given field_types as validators to check the fields + * @param array|null $fieldIds + * @return bool + * @throws ReflectionException + * @throws exception */ - public function setAction(string $value) : form { - $this->config['form_action'] = $value; - return $this; + public function isValid(array $fieldIds = null): bool + { + $tFields = $this->fields; + + foreach ($this->fieldsets as $fieldset) { + $tFields = array_merge($tFields, $fieldset->getFields()); + } + + foreach ($tFields as $field) { + // skip noninput fields + if ($field->getConfig()->get('field_noninput') === true) { + continue; + } + + // skip internal sent-determination field + if ($field === $this->formSentField) { + continue; + } + + if (is_array($fieldIds)) { + if (sizeof($fieldIds) > 0) { + $currentFieldId = $field->getConfig()->get('field_id'); + if (in_array($currentFieldId, $fieldIds)) { + $index = array_search($currentFieldId, $fieldIds); + if ($index !== false) { + unset($fieldIds[$index]); // Remove it from the to-be-checked IDs + } + } else { + continue; + } + } else { + break; + } + } elseif ($field->getConfig()->get('field_ajax') === true) { + continue; + } + + + $fieldname = $field->getConfig()->get('field_name'); + + // Check for existence of the field + if ($field->isRequired() && !$this->fieldSent($field)) { + $this->errorstack->addError($fieldname, 'FIELD_NOT_SET'); + continue; + } + + $fieldtype = $field->getConfig()->get('field_datatype'); + + $displaytype = $field->getConfig()->get('field_type'); + if ($displaytype == 'checkbox') { + $fieldtype = 'boolean'; + } + if (is_null($fieldtype)) { + continue; + } + + if ($displaytype == 'form') { + $subform = $field->getConfig()->get('form'); + + // provide subform-related data to the subform + if ($subform instanceof form && $this->fieldValue($field) !== null) { + $subform->getErrorstack()->reset(); + $subform->setFormRequest($this->fieldValue($field)); + if (!$subform->isValid()) { + $this->errorstack->addError($fieldname, 'FIELD_INVALID', $subform->getErrorstack()->getErrors()); + } + } + } + + if (($value = $this->fieldValue($field)) != null) { + $validation = app::getValidator($fieldtype)->reset()->validate($value); + if (count($validation) > 0) { + $this->errorstack->addError($fieldname, 'FIELD_INVALID', $validation); + } elseif (in_array($displaytype, ['select', 'radiogroup'])) { + // + // check a selected element if field_elements is existing + // + $fieldElements = $field->getConfig()->get('field_elements'); + if (is_array($fieldElements) && count($fieldElements) > 0) { + $fieldElementValues = array_column($fieldElements, $field->getConfig()->get('field_valuefield') ?? 'value'); + if (count($fieldElementValues) > 0) { + if (is_array($value)) { + foreach ($value as $valueElement) { + if (!in_array($valueElement, $fieldElementValues)) { + // error for wrong field value + $this->errorstack->addError($fieldname, 'FIELD_INVALID', $valueElement); + break; + } + } + } elseif (!in_array($value, $fieldElementValues)) { + // error for wrong field value + $this->errorstack->addError($fieldname, 'FIELD_INVALID', $value); + } + } + } + } + } + } + + if (is_array($fieldIds) && sizeof($fieldIds) > 0) { + // some field ids do not exist - $fieldIds has to be of size zero here. + throw new exception(self::EXCEPTION_FORM_VALIDATION_SELECTIVE_UNKNOWN_FIELD_IDS, exception::$ERRORLEVEL_ERROR, $fieldIds); + } + + // FIRE ON VALIDATION HOOK + $this->fireCallback(form::CALLBACK_FORM_VALIDATION); + + return $this->errorstack->isSuccess(); } /** - * Adds a $fieldset to the instance - * @param fieldset $fieldset - * @param int $position [position where to insert the fieldset; -1 is last, -2 the second last] - * @return form + * Returns all the fields in the form instance + * @return field[] */ - public function addFieldset(fieldset $fieldset, int $position = -1) : form { - if($position !== -1) { - array_splice($this->fieldsets, $position, 0, [ $fieldset ]); - return $this; - } else { - array_push($this->fieldsets, $fieldset); - return $this; - } + public function getFields(): array + { + return $this->fields; } /** - * Returns the array of fieldsets here - * @return fieldset[] + * Returns true if the given $field has been submitted in the las request + * Uses the request object to find the field's name + * @param field $field + * @return bool */ - public function getFieldsets() : array { - return $this->fieldsets; + public function fieldSent(field $field): bool + { + switch ($field->getConfig()->get('field_type')) { + case 'file' : + case 'signature' : // temporary fix for signature fields + $requestInstance = app::getRequest(); + if ($requestInstance instanceof filesInterface) { + return array_key_exists($field->getConfig()->get('field_name'), $requestInstance->getFiles()); + } else { + return array_key_exists($field->getConfig()->get('field_name'), $_FILES); + } + // no break + default: + if ($this->getFormRequest()->isDefined($field->getConfig()->get('field_name'))) { + if (is_array($value = $this->getFormRequest()->getData($field->getConfig()->get('field_name'))) && count($value) === 0) { + return false; + } else { + return true; + } + } else { + return false; + } + } } /** - * I will overwrite the $callback for the given $identifier. - *
Please add Callbacks by requiring an instance of \codename\core\ui\form named $form. - * @example ->addCallback(\codename\core\ui\form::CALLBACK_FORM_NOT_SENT, function(\codename\core\ui\form $form) {die('OK!');}); - * @return \codename\core\ui\form + * [getFormRequest description] + * @return request [description] */ - public function addCallback(string $identifier, callable $callback) : \codename\core\ui\form { - $this->callbacks[$identifier] = $callback; - return $this; + protected function getFormRequest(): datacontainer + { + if ($this->requestData == null) { + $this->setFormRequest(app::getRequest()->getData()); + } + return $this->requestData; } /** - * I will try accessing the callback identified by the given $identifier. - *
I will pass the current instance of \codename\core\ui\form to the callback method named $form - *
If the desired callback does not exist, I will do nothing. - * @return \codename\core\ui\form + * [setFormRequest description] + * @param array $requestData [description] */ - private function fireCallback(string $identifier) : \codename\core\ui\form { - if(array_key_exists($identifier, $this->callbacks)) { - call_user_func($this->callbacks[$identifier], $this); - } - return $this; + public function setFormRequest(array $requestData): void + { + $this->requestData = new datacontainer($requestData); } /** - * I am a standardized method for working off an existing form instance. - *
By default, the FORM_NOT_SENT callback will use the form template to output it. - *
By default, the FORM_NOT_VALID callback will output the occured errors using the standard outputs. - * @return \codename\core\ui\form + * Returns the data stored in the form + * Will return the whole dataset if you don't supply a $key + * Will return null if the given $key does not exist in the dataset + * @param string $key + * @return mixed */ - public function work() : \codename\core\ui\form { - if(!$this->isSent()) { - $this->fireCallback(\codename\core\ui\form::CALLBACK_FORM_NOT_SENT); - return $this; - } - - if(!$this->isValid()) { - $this->fireCallback(\codename\core\ui\form::CALLBACK_FORM_NOT_VALID); - return $this; + public function getData(string $key = ''): mixed + { +// if (is_null($this->data)) { +// foreach ($this->fields as $field) { +// $this->data->setData($field->getConfig()->get('field_name'), $this->fieldValue($field)); +// } +// } + $data = $this->getFormRequest()->getData(); + if (isset($_FILES)) { + $requestInstance = app::getRequest(); + if ($requestInstance instanceof filesInterface) { + $data = array_merge($data, $requestInstance->getFiles()); + } else { + $data = array_merge($data, $_FILES); + } } + $this->data->addData($data); + return $this->data->getData($key); + } - $this->fireCallback(\codename\core\ui\form::CALLBACK_FORM_VALID); - - return $this; + /** + * Returns the given $field instance's value depending on it's datatype + * @param field $field [description] + * @return mixed [description] + */ + public function fieldValue(field $field): mixed + { + return match ($field->getConfig()->get('field_type')) { + 'checkbox' => $this->getFormRequest()->isDefined($field->getConfig()->get('field_name')), + 'file' => $_FILES[$field->getConfig()->get('field_name')] ?? null, + default => $this->getFormRequest()->getData($field->getConfig()->get('field_name')), + }; } /** * returns a field instance based on a search for the given field name * or null, if not found - * @param string $fieldName [description] - * @return \codename\core\ui\field|null + * @param string $fieldName [description] + * @return field|null */ - public function getField(string $fieldName) : ?\codename\core\ui\field { - foreach($this->getFields() as $field) { - if($field->getProperty('field_name') == $fieldName) { - return $field; + public function getField(string $fieldName): ?field + { + foreach ($this->getFields() as $field) { + if ($field->getConfig()->get('field_name') == $fieldName) { + return $field; + } } - } - foreach($this->getFieldsets() as $fieldset) { - foreach($fieldset->getFields() as $field) { - if($field->getProperty('field_name') == $fieldName) { - return $field; - } + foreach ($this->getFieldsets() as $fieldset) { + foreach ($fieldset->getFields() as $field) { + if ($field->getConfig()->get('field_name') == $fieldName) { + return $field; + } + } } - } - return null; + return null; + } + + /** + * Returns the array of fieldsets here + * @return fieldset[] + */ + public function getFieldsets(): array + { + return $this->fieldsets; } /** - * returns a field instance based on a path (as array) + * returns a field instance based on a path (as an array) * or null, if not found - * @param array $fieldPath [description] - * @return \codename\core\ui\field|null - */ - public function getFieldRecursive(array $fieldPath) : ?\codename\core\ui\field { - $fieldName = array_shift($fieldPath); - foreach($this->getFields() as $field) { - if($field->getProperty('field_name') == $fieldName) { - if(count($fieldPath) === 0) { - // end reached - return $field; - } else { - // if we get a property named "form" - // dive deeper - if($form = $field->getProperty('form')) { - if($form instanceof \codename\core\ui\form) { - return $form->getFieldRecursive($fieldPath); - } else { - throw new exception('FORM_GETFIELDRECURSIVE_INVALID_FORM_INSTANCE', exception::$ERRORLEVEL_ERROR, $fieldName); - } - } else { - throw new exception('FORM_GETFIELDRECURSIVE_NO_FORM_INSTANCE', exception::$ERRORLEVEL_ERROR, $fieldName); + * @param array $fieldPath [description] + * @return field|null + * @throws exception + */ + public function getFieldRecursive(array $fieldPath): ?field + { + $fieldName = array_shift($fieldPath); + foreach ($this->getFields() as $field) { + if ($field->getConfig()->get('field_name') == $fieldName) { + if (count($fieldPath) === 0) { + // end reached + return $field; + } elseif ($form = $field->getConfig()->get('form')) { + // if we get a property named "form" + // dive deeper + if ($form instanceof form) { + return $form->getFieldRecursive($fieldPath); + } else { + throw new exception('FORM_GETFIELDRECURSIVE_INVALID_FORM_INSTANCE', exception::$ERRORLEVEL_ERROR, $fieldName); + } + } else { + throw new exception('FORM_GETFIELDRECURSIVE_NO_FORM_INSTANCE', exception::$ERRORLEVEL_ERROR, $fieldName); + } } - } } - } - foreach($this->getFieldsets() as $fieldset) { - foreach($fieldset->getFields() as $field) { - if($field->getProperty('field_name') == $fieldName) { - if(count($fieldPath) === 0) { - // end reached - return $field; - } else { - // if we get a property named "form" - // dive deeper - if($form = $field->getProperty('form')) { - if($form instanceof \codename\core\ui\form) { - return $form->getFieldRecursive($fieldPath); - } else { - throw new exception('FORM_GETFIELDRECURSIVE_INVALID_FORM_INSTANCE', exception::$ERRORLEVEL_ERROR, $fieldName); + foreach ($this->getFieldsets() as $fieldset) { + foreach ($fieldset->getFields() as $field) { + if ($field->getConfig()->get('field_name') == $fieldName) { + if (count($fieldPath) === 0) { + // end reached + return $field; + } elseif ($form = $field->getConfig()->get('form')) { + // if we get a property named "form" + // dive deeper + if ($form instanceof form) { + return $form->getFieldRecursive($fieldPath); + } else { + throw new exception('FORM_GETFIELDRECURSIVE_INVALID_FORM_INSTANCE', exception::$ERRORLEVEL_ERROR, $fieldName); + } + } else { + throw new exception('FORM_GETFIELDRECURSIVE_NO_FORM_INSTANCE', exception::$ERRORLEVEL_ERROR, $fieldName); + } } - } else { - throw new exception('FORM_GETFIELDRECURSIVE_NO_FORM_INSTANCE', exception::$ERRORLEVEL_ERROR, $fieldName); - } } - } } - } - return null; + return null; } /** - * @inheritDoc + * {@inheritDoc} * custom serialization to allow bare config form output */ - public function jsonSerialize() + public function jsonSerialize(): mixed { - return [ - 'config' => $this->config, - 'fields' => $this->fields, - 'fieldsets' => $this->fieldsets, - 'errorstack' => $this->errorstack - ]; + return [ + 'config' => $this->config, + 'fields' => $this->fields, + 'fieldsets' => $this->fieldsets, + 'errorstack' => $this->errorstack, + ]; } - } diff --git a/backend/class/frontend.php b/backend/class/frontend.php index e0825cd..6690a00 100644 --- a/backend/class/frontend.php +++ b/backend/class/frontend.php @@ -1,6 +1,12 @@ getNavigation(); - if(!$config->exists($key)) { + if (!$config->exists($key)) { return $output; } - foreach($config->get($key) as $element) { - if($element['type'] == 'group') { + foreach ($config->get($key) as $element) { + if ($element['type'] == 'group') { $output .= $this->parseGroup($element); continue; - } else if($element['type'] == 'iframe') { - $output .= $this->parseIframe($element); - continue; + } elseif ($element['type'] == 'iframe') { + $output .= $this->parseIframe($element); + continue; } $output .= $this->parseLink($element); } @@ -57,130 +58,145 @@ public function outputNavigation(string $key) : string { return $output; } + /** + * Returns navigation config nested in an object of type \codename\core\config + * @return config + */ + protected function getNavigation(): config + { + return new json('config/navigation.json'); + } + /** * Parses a navigation group * @param array $group * @return string + * @throws ReflectionException + * @throws exception */ - protected function parseGroup(array $group) : string { - + protected function parseGroup(array $group): string + { // - // Evaulate context permissions + // Evaluate context permissions // - if($group['context']) { - $allowedContextGroup = app::getConfig()->get('context>' . $group['context'] . '>_security>group'); - if($allowedContextGroup) { - if(!(app::getAuth()->isAuthenticated() && app::getAuth()->memberOf($allowedContextGroups))) { - return ''; + if ($group['context'] ?? false) { + $allowedContextGroup = app::getConfig()->get('context>' . $group['context'] . '>_security>group'); + if ($allowedContextGroup) { + if (!(app::getAuth()->isAuthenticated() && app::getAuth()->memberOf($allowedContextGroup))) { + return ''; + } } - } } // - // Evaulate view permissions + // Evaluate view permissions // - if($group['view']) { - $allowedViewGroup = app::getConfig()->get('context>' . $group['context'] . '>view>' .$group['view'] . '>_security>group'); - if($allowedViewGroup) { - if(!(app::getAuth()->isAuthenticated() && app::getAuth()->memberOf($allowedViewGroup))) { - return ''; + if ($group['view'] ?? false) { + $allowedViewGroup = app::getConfig()->get('context>' . $group['context'] . '>view>' . $group['view'] . '>_security>group'); + if ($allowedViewGroup) { + if (!(app::getAuth()->isAuthenticated() && app::getAuth()->memberOf($allowedViewGroup))) { + return ''; + } } - } } $filteredChildren = []; - foreach($group['children'] as $key => $child) { - // - // Evaulate context permissions - // - $allowedContextGroup = app::getConfig()->get('context>' . $child['context'] . '>_security>group'); - if($allowedContextGroup) { - if(!(app::getAuth()->isAuthenticated() && app::getAuth()->memberOf($allowedContextGroups))) { - continue; + foreach ($group['children'] as $key => $child) { + // + // Evaluate context permissions + // + $allowedContextGroup = app::getConfig()->get('context>' . $child['context'] . '>_security>group'); + if ($allowedContextGroup) { + if (!(app::getAuth()->isAuthenticated() && app::getAuth()->memberOf($allowedContextGroup))) { + continue; + } } - } - - // - // Evaulate view permissions - // - $allowedViewGroup = app::getConfig()->get('context>' . $child['context'] . '>view>' .$child['view'] . '>_security>group'); - if($allowedViewGroup) { - if(!(app::getAuth()->isAuthenticated() && app::getAuth()->memberOf($allowedViewGroup))) { - continue; + + // + // Evaluate view permissions + // + $allowedViewGroup = app::getConfig()->get('context>' . $child['context'] . '>view>' . $child['view'] . '>_security>group'); + if ($allowedViewGroup) { + if (!(app::getAuth()->isAuthenticated() && app::getAuth()->memberOf($allowedViewGroup))) { + continue; + } } - } - $filteredChildren[$key] = $child; + $filteredChildren[$key] = $child; } $group['children'] = $filteredChildren; $templateengine = 'default'; return app::getTemplateEngine($templateengine)->render('template/' . app::getRequest()->getData('template') . '/mainnavi/group', $group); - // return app::parseFile(app::getInheritedPath('frontend/template/' . app::getRequest()->getData('template') . '/mainnavi/group.php'), $group); } /** - * Parses a single link - * @param array $link + * Parses a dropdown containing an iframe + * @param array $action * @return string + * @throws ReflectionException + * @throws exception */ - protected function parseLink(array $link) : string { - // - // Evaulate context permissions - // - $allowedContextGroup = app::getConfig()->get('context>' . $link['context'] . '>_security>group'); - if($allowedContextGroup) { - if(!(app::getAuth()->isAuthenticated() && app::getAuth()->memberOf($allowedContextGroups))) { - return ''; - } - } - - // - // Evaulate view permissions - // - $allowedViewGroup = app::getConfig()->get('context>' . $link['context'] . '>view>' .$link['view'] . '>_security>group'); - if($allowedViewGroup) { - if(!(app::getAuth()->isAuthenticated() && app::getAuth()->memberOf($allowedViewGroup))) { - return ''; - } - } - - $templateengine = 'default'; - return app::getTemplateEngine($templateengine)->render('template/' . app::getRequest()->getData('template') . '/mainnavi/link', $link); - // return app::parseFile(app::getInheritedPath('frontend/template/' . app::getRequest()->getData('template') . '/mainnavi/link.php'), $link); + protected function parseIframe(array $action): string + { + $templateengine = 'default'; + return app::getTemplateEngine($templateengine)->render('template/' . app::getRequest()->getData('template') . '/mainnavi/iframe', $action); } /** - * Parses a dropdown containing an iframe - * @param array $action + * Parses a single link + * @param array $link * @return string + * @throws ReflectionException + * @throws exception */ - protected function parseIframe(array $action) : string { - $templateengine = 'default'; - return app::getTemplateEngine($templateengine)->render('template/' . app::getRequest()->getData('template') . '/mainnavi/iframe', $action); - // return app::parseFile(app::getInheritedPath('frontend/template/' . app::getRequest()->getData('template') . '/mainnavi/iframe.php'), $action); + protected function parseLink(array $link): string + { + // + // Evaluate context permissions + // + $allowedContextGroup = app::getConfig()->get('context>' . $link['context'] . '>_security>group'); + if ($allowedContextGroup) { + if (!(app::getAuth()->isAuthenticated() && app::getAuth()->memberOf($allowedContextGroup))) { + return ''; + } + } + + // + // Evaluate view permissions + // + $allowedViewGroup = app::getConfig()->get('context>' . $link['context'] . '>view>' . $link['view'] . '>_security>group'); + if ($allowedViewGroup) { + if (!(app::getAuth()->isAuthenticated() && app::getAuth()->memberOf($allowedViewGroup))) { + return ''; + } + } + + $templateengine = 'default'; + return app::getTemplateEngine($templateengine)->render('template/' . app::getRequest()->getData('template') . '/mainnavi/link', $link); } /** * Returns a complete configuration * @param string $groupname - * @throws \codename\core\exception * @return string + * @throws ReflectionException + * @throws exception */ - public function getGroup(string $groupname) : string { + public function getGroup(string $groupname): string + { $data = $this->getNavigation(); - if(!$data->exists("group")) { - throw new \codename\core\exception(self::EXCEPTION_GETGROUP_NOGROUPSAVAILABLE, \codename\core\exception::$ERRORLEVEL_ERROR, null); + if (!$data->exists("group")) { + throw new exception(self::EXCEPTION_GETGROUP_NOGROUPSAVAILABLE, exception::$ERRORLEVEL_ERROR, null); } - if(!$data->exists("group>{$groupname}")) { - throw new \codename\core\exception(self::EXCEPTION_GETGROUP_GROUPNOTFOUND, \codename\core\exception::$ERRORLEVEL_ERROR, $groupname); + if (!$data->exists("group>$groupname")) { + throw new exception(self::EXCEPTION_GETGROUP_GROUPNOTFOUND, exception::$ERRORLEVEL_ERROR, $groupname); } - return app::getTemplateEngine($templateengine)->render('template/' . app::getRequest()->getData('template') . '/groupnavi/group', $data->get("group>{$groupname}")); - // return app::parseFile(app::getInheritedPath('frontend/template/' . app::getRequest()->getData('template') . '/groupnavi/group.php'), $data->get("group>{$groupname}")); + $templateengine = 'default'; + return app::getTemplateEngine($templateengine)->render('template/' . app::getRequest()->getData('template') . '/groupnavi/group', $data->get("group>$groupname")); } - } diff --git a/backend/class/frontend/element.php b/backend/class/frontend/element.php index 7c8fc59..675a130 100644 --- a/backend/class/frontend/element.php +++ b/backend/class/frontend/element.php @@ -1,88 +1,99 @@ configValidatorName); - } +abstract class element +{ + /** + * exception that identities an invalid config + * @var string + */ + public const string EXCEPTION_CORE_UI_FRONTEND_ELEMENT_INVALID_CONFIG = 'EXCEPTION_CORE_UI_FRONTEND_ELEMENT_INVALID_CONFIG'; + /** + * data + * @var datacontainer + */ + public datacontainer $data; + /** + * config + * @var config + */ + protected config $config; + /** + * validator used for validating the given configuration + * @var string + */ + protected $configValidatorName = 'structure_config_frontend_element'; + /** + * templatePath + * @var string + */ + protected $templatePath; - /** - * [__construct description] - * @param array $config [description] - */ - public function __construct(array $config = array(), array $data = array()) { - if(count($errors = $this->getValidator()->reset()->validate($config)) === 0) { - $this->config = new \codename\core\config($config); - } else { - throw new exception(self::EXCEPTION_CORE_UI_FRONTEND_ELEMENT_INVALID_CONFIG, exception::$ERRORLEVEL_ERROR, $config); + /** + * [__construct description] + * @param array $config [description] + * @param array $data + * @throws ReflectionException + * @throws exception + */ + public function __construct(array $config = [], array $data = []) + { + if (count($this->getValidator()->reset()->validate($config)) === 0) { + $this->config = new config($config); + } else { + throw new exception(self::EXCEPTION_CORE_UI_FRONTEND_ELEMENT_INVALID_CONFIG, exception::$ERRORLEVEL_ERROR, $config); + } + $this->data = new datacontainer($data); } - $this->data = new datacontainer($data); - } - /** - * exception that idenfities an invalid config - * @var string - */ - const EXCEPTION_CORE_UI_FRONTEND_ELEMENT_INVALID_CONFIG = 'EXCEPTION_CORE_UI_FRONTEND_ELEMENT_INVALID_CONFIG'; - - /** - * [handleData description] - * @return array [description] - */ - protected function handleData() : array { - return $this->data()->getData(); - } - - /** - * [public description] - * @var [type] - */ - public function outputData() : array { - return $this->handleData(); - } + /** + * [getValidator description] + * @return validator [description] + * @throws ReflectionException + * @throws exception + */ + protected function getValidator(): validator + { + return app::getValidator($this->configValidatorName); + } - /** - * string output - * @return string - */ - public function outputString() : string { - return app::getTemplateEngine($this->config->get('templateengine', 'default'))->render($this->templatePath, $this->handleData()); - } + /** + * [public description] + * @var [type] + */ + public function outputData(): array + { + return $this->handleData(); + } - /** - * templatePath - * @var string - */ - protected $templatePath; + /** + * [handleData description] + * @return array [description] + */ + protected function handleData(): array + { + return $this->data->getData(); + } + /** + * string output + * @return string + * @throws ReflectionException + * @throws exception + */ + public function outputString(): string + { + return app::getTemplateEngine($this->config->get('templateengine', 'default'))->render($this->templatePath, $this->handleData()); + } } diff --git a/backend/class/frontend/element/table.php b/backend/class/frontend/element/table.php index d4ee097..add8d8a 100644 --- a/backend/class/frontend/element/table.php +++ b/backend/class/frontend/element/table.php @@ -1,91 +1,86 @@ templatePath = 'element/table/default'; - parent::__construct($config, $data); + $this->templatePath = 'element/table/default'; + parent::__construct($config, $data); } /** - * @inheritDoc + * {@inheritDoc} */ protected function handleData(): array { - // we assume rows and key => value data in this table by default. @TODO: change this to auto-recognize it? (e.g. assoc array) - - $columns = array(); - $rows = array(); - - $data = $this->data->getData(); - - if($this->config->exists('columns')) { - // defined columns - $columns = $this->config->get('columns'); - } else { - // autogenerate columns - foreach($data as $key => $value) { - if(!is_string($key) && is_numeric($key)) { - // numeric index => ROWS! - // - $columns = array_unique(array_merge($columns, array_keys($value))); - } else { - // ASSOC array! - // key => value ! - - } + // we assume rows and key => value data in this table by default. @TODO: change this to auto-recognize it? (e.g., assoc array) + + $columns = []; + + $data = $this->data->getData(); + + if ($this->config->exists('columns')) { + // defined columns + $columns = $this->config->get('columns'); + } else { + // autogenerate columns + foreach ($data as $key => $value) { + if (!is_string($key) && is_numeric($key)) { + // numeric index => ROWS! + // + $columns = array_unique(array_merge($columns, array_keys($value))); + } else { + // ASSOC array! + // key => value ! + } + } } - } - - // generate table - // - $table = [ - 'max' => [], - 'header' => $columns, - 'rows' => [], - 'footer' => [] - ]; - foreach($columns as $col) { - $table['max'][$col] = strlen($col); - } - - foreach($data as $index => $indexValue) { - - $rowValues = []; - foreach($indexValue as $key => $value) { - if(in_array($key, $columns)) { - - // convert non-string to string, somehow - if(!is_string($value)) { - $value = print_r($value, true); - } + // generate table + // + $table = [ + 'max' => [], + 'header' => $columns, + 'rows' => [], + 'footer' => [], + ]; + + foreach ($columns as $col) { + $table['max'][$col] = strlen($col); + } - // detect max column value length - // for cli output... - if(strlen($value) > $table['max'][$key]) { - $table['max'][$key] = strlen($value); + foreach ($data as $indexValue) { + $rowValues = []; + foreach ($indexValue as $key => $value) { + if (in_array($key, $columns)) { + // convert non-string to string, somehow + if (!is_string($value)) { + $value = print_r($value, true); + } + + // detect max column value length + // for cli output... + if (strlen($value) > $table['max'][$key]) { + $table['max'][$key] = strlen($value); + } + + $rowValues[$key] = $value; + } } - $rowValues[$key] = $value; - } + $table['rows'][] = $rowValues; } - $table['rows'][] = $rowValues; - } - - // print_r($this->data); - // print_r($table); - - return $table; + return $table; } - } diff --git a/backend/class/helper/context.php b/backend/class/helper/context.php new file mode 100644 index 0000000..afc4506 --- /dev/null +++ b/backend/class/helper/context.php @@ -0,0 +1,107 @@ +getCount(); + } + + // default value, if none of the below works: + $page = 1; + if (app::getRequest()->isDefined('pagination_page')) { + // explicit page request + $page = (int)app::getRequest()->getData('pagination_page'); + } + + if (app::getRequest()->isDefined('pagination_limit')) { + $limit = (int)app::getRequest()->getData('pagination_limit'); + } else { + $limit = self::$paginationDefaultLimit; + } + + if (!in_array($limit, self::$paginationPageSizes)) { + // default fallback + $limit = self::$paginationDefaultLimit; + } + + $pages = ($limit == 0 || $count == 0) ? 1 : ceil($count / $limit); + + // pagination limit change with present page param, that is out of range: + if ($page > $pages) { + $page = $pages; + } + + if ($pages > 1) { + if (is_array($modelPagination)) { + $modelPagination = array_slice($modelPagination, ($page - 1) * $limit, $limit); + } else { + $modelPagination->setLimit($limit)->setOffset(($page - 1) * $limit); + } + } + + app::getResponse()->setData('pagination', [ + 'pagination_count' => $count, + 'pagination_page' => $page, + 'pagination_page_count' => $pages, + 'pagination_sizes' => self::$paginationPageSizes, + 'pagination_limit' => $limit, + ]); + } + +} diff --git a/backend/class/templateengine/dummy.php b/backend/class/templateengine/dummy.php index 30cd2ec..caa9a95 100644 --- a/backend/class/templateengine/dummy.php +++ b/backend/class/templateengine/dummy.php @@ -1,41 +1,46 @@ render("view/" . $viewPath, $data); - } + /** + * {@inheritDoc} + * @param string $viewPath + * @param object|array|null $data + * @return string + * @throws ReflectionException + * @throws exception + */ + public function renderView(string $viewPath, object|array|null $data = null): string + { + return $this->render("view/" . $viewPath, $data); + } - /** - * @inheritDoc - */ - public function renderTemplate( string $templatePath, $data = null): string { - return $this->render("template/" . $templatePath . "/template", $data); - } + /** + * {@inheritDoc} + * @param string $referencePath + * @param object|array|null $data + * @return string + * @throws ReflectionException + * @throws exception + */ + public function render(string $referencePath, object|array|null $data = null): string + { + return app::parseFile(app::getInheritedPath("frontend/" . $referencePath . ".php"), $data); + } -} \ No newline at end of file + /** + * {@inheritDoc} + * @param string $templatePath + * @param object|array|null $data + * @return string + * @throws ReflectionException + * @throws exception + */ + public function renderTemplate(string $templatePath, object|array|null $data = null): string + { + return $this->render("template/" . $templatePath . "/template", $data); + } +} diff --git a/backend/class/templateengine/twig.php b/backend/class/templateengine/twig.php index 7cb93d4..99511df 100644 --- a/backend/class/templateengine/twig.php +++ b/backend/class/templateengine/twig.php @@ -1,440 +1,511 @@ true, - 'sandbox_mode' => 'global', - 'sandbox' => [ - 'tags' => [ - 'if', - 'for', - ], - 'functions' => [ - 'include', - 'template_from_string', - ], - ] - ]; - - /** - * Default sandbox - * with some minimalistic stuff - * allowing include + template_from_string - * only enabled when using sandbox rendering explicitly - * @var array - */ - public const DefaultConfigSandboxWithIncludes = [ - 'sandbox_enabled' => true, - 'sandbox_mode' => null, - 'sandbox' => [ - 'tags' => [ - 'if', - 'for', +class twig extends templateengine implements clientInterface +{ + /** + * Default global sandbox + * with some minimalistic stuff + * allowing to include + template_from_string + * @var array + */ + public const array DefaultConfigSandboxGlobalWithIncludes = [ + 'sandbox_enabled' => true, + 'sandbox_mode' => 'global', + 'sandbox' => [ + 'tags' => [ + 'if', + 'for', + ], + 'functions' => [ + 'include', + 'template_from_string', + ], ], - 'functions' => [ - 'include', - 'template_from_string', + ]; + + /** + * Default sandbox + * with some minimalistic stuff + * allowing to include + template_from_string + * only enabled when using sandbox rendering explicitly + * @var array + */ + public const array DefaultConfigSandboxWithIncludes = [ + 'sandbox_enabled' => true, + 'sandbox_mode' => null, + 'sandbox' => [ + 'tags' => [ + 'if', + 'for', + ], + 'functions' => [ + 'include', + 'template_from_string', + ], ], - ] - ]; - - /** - * its very own client name - * @var [type] - */ - protected $clientName = null; - - /** - * @inheritDoc - */ - public function setClientName(string $name) - { - if($this->clientName == null) { - $this->clientName = $name; - $this->twigInstance->setTemplateClassPrefixPrefix($this->clientName); - } else { - throw new exception("EXCEPTION_CORE_CLIENT_INTERFACE_CANNOT_RENAME_CLIENT", exception::$ERRORLEVEL_FATAL, $this->clientName); - } - } - - /** - * @inheritDoc - */ - public function getClientName(string $name) - { - return $this->clientName; - } - - /** - * twig instance - * @var \codename\core\ui\templateengine\twig\environment\core - */ - protected $twigInstance = null; - - /** - * twig loader - * @var \Twig\Loader\LoaderInterface - */ - protected $twigLoader = null; - - /** - * File extension automatically added for finding extensions - * @var string - */ - protected $templateFileExtension = '.twig'; - - /** - * @inheritDoc - */ - public function __construct(array $config = array()) - { - // Check for existance of Twig Classes. - if (!class_exists('\\Twig\\Environment')) { - throw new exception("CORE_TEMPLATEENGINE_TWIG_CLASS_DOES_NOT_EXIST", exception::$ERRORLEVEL_FATAL); - } + ]; + + /** + * its very own client name + * @var string|null [type] + */ + protected ?string $clientName = null; + /** + * twig instance + * @var core + */ + protected core $twigInstance; + /** + * File extension automatically added for finding extensions + * @var string + */ + protected string $templateFileExtension = '.twig'; + /** + * [protected description] + * @var null|SandboxExtension + */ + protected ?SandboxExtension $sandboxExtensionInstance = null; + /** + * Sandbox mode override + * @var bool + */ + protected bool $sandboxOverride = false; - // Default asset dir - // required for explicit images, css, etc. referenced from template - $config['assets_path'] = $config['assets_path'] ?? 'twig_assets_path'; + /** + * {@inheritDoc} + * @param array $config + * @throws LoaderError + * @throws ReflectionException + * @throws RuntimeError + * @throws exception + */ + public function __construct(array $config = []) + { + // Check for the existence of Twig Classes. + if (!class_exists('\\Twig\\Environment')) { + throw new exception("CORE_TEMPLATEENGINE_TWIG_CLASS_DOES_NOT_EXIST", exception::$ERRORLEVEL_FATAL); + } - parent::__construct($config); + // Default asset dir + // required for explicit images, CSS, etc. referenced from template + $config['assets_path'] = $config['assets_path'] ?? 'twig_assets_path'; - if(!empty($config['template_file_extension'])) { - $this->templateFileExtension = $config['template_file_extension']; - } + parent::__construct($config); - $paths = array(); - - // collect appstack paths - // to search for views in - // this includes the current app - // so, no need to add it explicitly - foreach(app::getAppstack() as $parentapp) { - $vendor = $parentapp['vendor']; - $app = $parentapp['app']; - if($vendor != 'corename' && $app != 'core') { - $appDir = app::getHomedir($vendor, $app); - $dir = $appDir . 'frontend/'; - - // the frontend root dir has to exist - // otherwise, we're adding it to the paths - // for Twig to search for templates in - if(app::getFilesystem()->dirAvailable($dir)) { - $paths[] = $dir; + if (!empty($config['template_file_extension'])) { + $this->templateFileExtension = $config['template_file_extension']; } - } - } - // Configure path suffixing only for FS Loader - $fsLoader = new \codename\core\ui\templateengine\twig\loader\filesystem($paths, CORE_VENDORDIR); - $fsLoader->templateFileSuffix = $this->templateFileExtension; - $this->twigLoader = $fsLoader; + $paths = []; + + // collect appstack paths + // to search for views in + // this includes the current app + // so, no need to add it explicitly + foreach (app::getAppstack() as $parentapp) { + $vendor = $parentapp['vendor']; + $app = $parentapp['app']; + if ($vendor != 'corename' && $app != 'core') { + $appDir = app::getHomedir($vendor, $app); + $dir = $appDir . 'frontend/'; + + // the frontend root dir has to exist + // otherwise; we're adding it to the paths + // for Twig to search for templates in + if (app::getFilesystem()->dirAvailable($dir)) { + $paths[] = $dir; + } + } + } - /** - * Important Note: - * we're using a custom class as template base - * to support relative paths in embed/include/... twig blocks - */ - $options = array_merge( - array( - 'base_template_class' => '\\codename\\core\\ui\\templateengine\\twig\\template\\baseTemplate' - ), - $config['environment'] ?? array() - ); - - $this->twigInstance = new \codename\core\ui\templateengine\twig\environment\core($this->twigLoader, $options); - // $this->twigInstance->templateFileSuffix = $this->templateFileExtension; - - $extensions = []; - - $extensions[] = new extension\routing; - $extensions[] = new \Twig\Extensions\IntlExtension(); - $extensions[] = new \Twig\Extension\StringLoaderExtension(); - - if(!empty($config['sandbox_enabled']) && $config['sandbox_enabled']) { - $globalSandbox = !empty($config['sandbox_mode']) && $config['sandbox_mode'] == 'global'; - - $allowedTags = $config['sandbox']['tags'] ?? []; - $allowedFilters = array_merge($config['sandbox']['filters'] ?? [], [ 'escape' ]); // auto-escape needs this at all times - $allowedMethods = $config['sandbox']['methods'] ?? []; - $allowedProperties = $config['sandbox']['properties'] ?? []; - $allowedFunctions = $config['sandbox']['functions'] ?? []; - - $policy = new \Twig\Sandbox\SecurityPolicy( - $allowedTags, - $allowedFilters, - $allowedMethods, - $allowedProperties, - $allowedFunctions - ); - - $extensions[] = $this->sandboxExtensionInstance = new \Twig\Extension\SandboxExtension($policy, $globalSandbox); - } + $twigLoader = new FilesystemLoader($paths, CORE_VENDORDIR); - // - // Special sandbox overide for compatibility - // allows executing renderStringSandboxed without really using the sandbox - // - if(($config['sandbox_enabled'] ?? null) === false & (($config['sandbox_mode'] ?? null) === 'override')) { - $this->sandboxOverride = true; - } + $options = $config['environment'] ?? []; - $this->twigInstance->setExtensions($extensions); - - // Add request and response containers, globally - $this->twigInstance->addGlobal('request', app::getRequest()); - $this->twigInstance->addGlobal('response', app::getResponse()); - $this->twigInstance->addGlobal('frontend', \codename\core\ui\app::getInstance('frontend')); - - // - // workaround to perform on-demand init of translation client, only if defined in environment - // - if(app::getEnvironment()->get(app::getEnv().'>translate>default')) { - // - // NOTE: might fail in unconfigured env, as "inherit" config exists - // (bare core app without anything added) - // - try { - $this->twigInstance->addGlobal('translate', app::getTranslate('default')); - } catch (\Exception $e) { - // swallow exception. - } - } + $this->twigInstance = new core($twigLoader, $options); - // add testing for array - $this->twigInstance->addTest(new \Twig\TwigTest('array', function ($value) { - return is_array($value); - })); + $extensions = []; - // add testing for string - $this->twigInstance->addTest(new \Twig\TwigTest('string', function ($value) { - return is_string($value); - })); + $extensions[] = new extension\display($this->templateFileExtension); + $extensions[] = new extension\routing(); + $extensions[] = new IntlExtension(); + $extensions[] = new StringLoaderExtension(); + if (!empty($config['sandbox_enabled'])) { + $globalSandbox = !empty($config['sandbox_mode']) && $config['sandbox_mode'] == 'global'; - $assetsTempDir = $this->getAssetsPath(); + $allowedTags = $config['sandbox']['tags'] ?? []; + $allowedFilters = array_merge($config['sandbox']['filters'] ?? [], ['escape']); // auto-escape needs this at all times + $allowedMethods = $config['sandbox']['methods'] ?? []; + $allowedProperties = $config['sandbox']['properties'] ?? []; + $allowedFunctions = $config['sandbox']['functions'] ?? []; - // - // This is meant mostly for internal rendering purposes - // - $this->twigInstance->addFunction(new \Twig\TwigFunction('asset_path', function(\Twig\Environment $env, $name, bool $ignoreMissing = true) use ($assetsTempDir) { + $policy = new SecurityPolicy( + $allowedTags, + $allowedFilters, + $allowedMethods, + $allowedProperties, + $allowedFunctions + ); - $template = null; - // TODO: limit debug_backtrace ? - foreach (debug_backtrace() as $trace) { - if (isset($trace['object']) && $trace['object'] instanceof \Twig\Template && 'Twig_Template' !== get_class($trace['object'])) { - $template = $trace['object']; - break; // break on first one. + $extensions[] = $this->sandboxExtensionInstance = new SandboxExtension($policy, $globalSandbox); } - } - - $path = null; - $dir = null; - if($template) { - $templateName = $template->getTemplateName(); - $dir = pathinfo($templateName, PATHINFO_DIRNAME); - foreach(app::getAppstack() as $app) { - $path = realpath($tryPath = app::getHomedir($app['vendor'], $app['app']).'/frontend/'.$dir.'/'.$name); - if($path !== false) { - break; - } + + // + // Special sandbox override for compatibility + // allows executing renderStringSandboxed without really using the sandbox + // + if (($config['sandbox_enabled'] ?? null) === false & (($config['sandbox_mode'] ?? null) === 'override')) { + $this->sandboxOverride = true; } - } - - if($path) { - - $hash = md5($path); - $filename = pathinfo($name, PATHINFO_BASENAME); - $tmpFile = $hash.'_'.$filename; - $tmpFilePath = $assetsTempDir.$tmpFile; - if(!app::getFilesystem()->fileAvailable($tmpFilePath)) { - // copy to temp dir - if(!app::getFilesystem()->fileCopy($path, $tmpFilePath)) { - throw new exception('ASSET_COPY_FAILED', exception::$ERRORLEVEL_ERROR); - } - } else { - // exists. we MAY check integrity? + + $this->twigInstance->setExtensions($extensions); + + // Add request and response containers, globally + $this->twigInstance->addGlobal('request', app::getRequest()); + $this->twigInstance->addGlobal('response', app::getResponse()); + $this->twigInstance->addGlobal('frontend', \codename\core\ui\app::getInstance('frontend')); + + // + // workaround to perform on-demand init of translation client, only if defined in environment + // + if (app::getEnvironment()->get(app::getEnv() . '>translate>default')) { + // + // NOTE: might fail in unconfigured env, as "inherit" config exists + // (bare core app without anything added) + // + try { + $this->twigInstance->addGlobal('translate', app::getTranslate()); + } catch (\Exception) { + // swallow exception. + } } - return $tmpFilePath; - } else { - // error, not found? - throw new exception('ASSET_UNAVAILABLE', exception::$ERRORLEVEL_ERROR, $name); - } - }, [ - 'needs_environment' => true, - ])); + // add testing for an array + $this->twigInstance->addTest( + new TwigTest('array', function ($value) { + return is_array($value); + }) + ); + + // add testing for string + $this->twigInstance->addTest( + new TwigTest('string', function ($value) { + return is_string($value); + }) + ); + + + $assetsTempDir = $this->getAssetsPath(); + + // + // This is meant mostly for internal rendering purposes + // + $this->twigInstance->addFunction( + new TwigFunction('asset_path', function (Environment $env, string $name, bool $ignoreMissing = true) use ($assetsTempDir) { + $template = null; + // TODO: limit debug_backtrace ? + foreach (debug_backtrace() as $trace) { + if (isset($trace['object']) && $trace['object'] instanceof Template && 'Twig_Template' !== get_class($trace['object'])) { + $template = $trace['object']; + break; // break on the first one. + } + } + + $path = null; + if ($template) { + $templateName = $template->getTemplateName(); + $dir = pathinfo($templateName, PATHINFO_DIRNAME); + foreach (app::getAppstack() as $app) { + $path = realpath(app::getHomedir($app['vendor'], $app['app']) . '/frontend/' . $dir . '/' . $name); + if ($path !== false) { + break; + } + } + } + + if ($path) { + $hash = md5($path); + $filename = pathinfo($name, PATHINFO_BASENAME); + $tmpFile = $hash . '_' . $filename; + $tmpFilePath = $assetsTempDir . $tmpFile; + if (!app::getFilesystem()->fileAvailable($tmpFilePath)) { + // copy to temp dir + if (!app::getFilesystem()->fileCopy($path, $tmpFilePath)) { + throw new exception('ASSET_COPY_FAILED', exception::$ERRORLEVEL_ERROR); + } + } else { + // exists. we MAY check integrity? + } + + return $tmpFilePath; + } else { + // error, not found? + throw new exception('ASSET_UNAVAILABLE', exception::$ERRORLEVEL_ERROR, $name); + } + }, [ + 'needs_environment' => true, + ]) + ); + + $this->twigInstance->addFunction( + new TwigFunction('strpadleft', function ($string, $pad_length, $pad_string = " ") { + return str_pad($string, $pad_length, $pad_string, STR_PAD_LEFT); + }) + ); + + $this->twigInstance->addFunction( + new TwigFunction('strpadright', function ($string, $pad_length, $pad_string = " ") { + return str_pad($string, $pad_length, $pad_string); + }) + ); + + $this->twigInstance->addFunction( + new TwigFunction('var_export', function ($value) { + return var_export($value, true); + }) + ); + + $this->twigInstance->addFunction( + new TwigFunction('print_r', function ($value) { + return print_r($value, true); + }) + ); + + + if (app::getRequest() instanceof cli) { + $this->twigInstance->addFunction( + new TwigFunction('cli_format', function ($value, $color) { + return clicolors::getInstance()->getColoredString($value, $color); + }) + ); + } + } - $this->twigInstance->addFunction(new \Twig\TwigFunction('strpadleft', function($string, $pad_length, $pad_string = " ") { - return str_pad($string, $pad_length, $pad_string, STR_PAD_LEFT); - })); + /** + * {@inheritDoc} + */ + public function getAssetsPath(): string + { + return sys_get_temp_dir() . '/' . ($this->config->get('assets_path') ?? 'twig_assets_path') . '/'; + } - $this->twigInstance->addFunction(new \Twig\TwigFunction('strpadright', function($string, $pad_length, $pad_string = " ") { - return str_pad($string, $pad_length, $pad_string, STR_PAD_RIGHT); - })); + /** + * adds a function available during the render process + * @param string $name [description] + * @param callable $function [description] + */ + public function addFunction(string $name, callable $function): void + { + $this->twigInstance->addFunction(new TwigFunction($name, $function)); + } - $this->twigInstance->addFunction(new \Twig\TwigFunction('var_export', function($value) { - return var_export($value, true); - })); + /** + * {@inheritDoc} + */ + public function getClientName(string $name): string + { + return $this->clientName; + } - $this->twigInstance->addFunction(new \Twig\TwigFunction('print_r', function($value) { - return print_r($value, true); - })); + /** + * {@inheritDoc} + * @param string $name + * @throws exception + */ + public function setClientName(string $name): void + { + if ($this->clientName == null) { + $this->clientName = $name; + $this->twigInstance->setTemplateClassPrefixPrefix($this->clientName); + } else { + throw new exception("EXCEPTION_CORE_CLIENT_INTERFACE_CANNOT_RENAME_CLIENT", exception::$ERRORLEVEL_FATAL, $this->clientName); + } + } + /** + * [renderSandboxed description] + * @param string $referencePath [description] + * @param array $variableContext [description] + * @return string [description] + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError + * @throws Throwable + * @throws exception + */ + public function renderSandboxed(string $referencePath, array $variableContext): string + { + if (!$this->sandboxOverride && !$this->sandboxExtensionInstance) { + throw new exception('TEMPLATEENGINE_TWIG_NO_SANDBOX_INSTANCE', exception::$ERRORLEVEL_ERROR); + } - if(app::getRequest() instanceof \codename\core\request\cli) { - $this->twigInstance->addFunction(new \Twig\TwigFunction('cli_format', function($value, $color) { - return \codename\core\helper\clicolors::getInstance()->getColoredString($value, $color); - })); - } - } - - /** - * @inheritDoc - */ - public function getAssetsPath(): string - { - return sys_get_temp_dir() . '/' . ($this->config->get('assets_path') ?? 'twig_assets_path') . '/'; - } - - /** - * [protected description] - * @var \Twig\Extension\SandboxExtension - */ - protected $sandboxExtensionInstance = null; - - /** - * Sandbox mode override - * @var bool - */ - protected $sandboxOverride = false; - - /** - * adds a function available during the render process - * @param string $name [description] - * @param callable $function [description] - */ - public function addFunction(string $name, callable $function) { - $this->twigInstance->addFunction(new \Twig\TwigFunction($name, $function)); - } - - /** - * [renderSandboxed description] - * @param string $referencePath [description] - * @param array $variableContext [description] - * @return string [description] - */ - public function renderSandboxed(string $referencePath, array $variableContext) : string { - - if(!$this->sandboxOverride && !$this->sandboxExtensionInstance) { - throw new exception('TEMPLATEENGINE_TWIG_NO_SANDBOX_INSTANCE', exception::$ERRORLEVEL_ERROR); - } + $prevSandboxState = false; + if (!$this->sandboxOverride) { + // Store sandbox state + $prevSandboxState = $this->sandboxExtensionInstance->isSandboxed(); + } - if(!$this->sandboxOverride) { - // Store sandbox state - $prevSandboxState = $this->sandboxExtensionInstance->isSandboxed(); - } + // enable sandbox for a brief moment + if (!$this->sandboxOverride && !$prevSandboxState) { + $this->sandboxExtensionInstance->enableSandbox(); + } - // enable sandbox for a brief moment - if(!$this->sandboxOverride && !$prevSandboxState) { - $this->sandboxExtensionInstance->enableSandbox(); - } + // "template" must be of type string + $variableContext['template'] = $variableContext['template'] ?? ''; - $twigTemplate = $this->twigInstance->load($referencePath); - $rendered = $twigTemplate->render($variableContext); + $twigTemplate = $this->twigInstance->load($referencePath . $this->templateFileExtension); + $rendered = $twigTemplate->render($variableContext); - // disable sandbox again, if it has been disabled before - if(!$this->sandboxOverride && !$prevSandboxState) { - $this->sandboxExtensionInstance->disableSandbox(); - } + // disable sandbox again if it has been disabled before + if (!$this->sandboxOverride && !$prevSandboxState) { + $this->sandboxExtensionInstance->disableSandbox(); + } - return $rendered; - } - - /** - * [renderStringSandboxed description] - * @param string $template [description] - * @param array $variableContext [description] - * @return string [description] - */ - public function renderStringSandboxed(string $template, array $variableContext) : string { - if(!$this->sandboxOverride && !$this->sandboxExtensionInstance) { - throw new exception('TEMPLATEENGINE_TWIG_NO_SANDBOX_INSTANCE', exception::$ERRORLEVEL_ERROR); + return $rendered; } - if(!$this->sandboxOverride) { - // Store sandbox state - $prevSandboxState = $this->sandboxExtensionInstance->isSandboxed(); + /** + * {@inheritDoc} + * + * twig loads a custom element/partial/whatever like this (fixed:) + * frontend/.twig + * @param string $referencePath + * @param object|array|null $data + * @return string + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError + * @throws Throwable + */ + public function render(string $referencePath, object|array|null $data = null): string + { + $twigTemplate = $this->twigInstance->load($referencePath . $this->templateFileExtension); + try { + return $twigTemplate->render([ + 'data' => $data, + ]); + } catch (Throwable $e) { + throw $e->getPrevious() ?? new exception("CORE_TEMPLATEENGINE_TWIG_RENDER_INTERNAL_ERROR", exception::$ERRORLEVEL_FATAL); + } } - // enable sandbox for a brief moment - if(!$this->sandboxOverride && !$prevSandboxState) { - $this->sandboxExtensionInstance->enableSandbox(); - } + /** + * [renderStringSandboxed description] + * @param string $template [description] + * @param array $variableContext [description] + * @return string [description] + * @throws LoaderError + * @throws SyntaxError + * @throws Throwable + * @throws exception + */ + public function renderStringSandboxed(string $template, array $variableContext): string + { + if (!$this->sandboxOverride && !$this->sandboxExtensionInstance) { + throw new exception('TEMPLATEENGINE_TWIG_NO_SANDBOX_INSTANCE', exception::$ERRORLEVEL_ERROR); + } - $twigTemplate = $this->twigInstance->createTemplate($template); - $rendered = $twigTemplate->render($variableContext); + $prevSandboxState = false; + if (!$this->sandboxOverride) { + // Store sandbox state + $prevSandboxState = $this->sandboxExtensionInstance->isSandboxed(); + } - // disable sandbox again, if it has been disabled before - if(!$this->sandboxOverride && !$prevSandboxState) { - $this->sandboxExtensionInstance->disableSandbox(); - } + // enable sandbox for a brief moment + if (!$this->sandboxOverride && !$prevSandboxState) { + $this->sandboxExtensionInstance->enableSandbox(); + } - return $rendered; - } - - /** - * @inheritDoc - * - * twig loads a custom element/partial/whatever like this (fixed:) - * frontend/.twig - */ - public function render(string $referencePath, $data = null): string { - $twigTemplate = $this->twigInstance->load($referencePath); - try { - return $twigTemplate->render(array( - 'data' => $data - )); - } catch (\Twig\Error\RuntimeError $e) { - throw $e->getPrevious(); + // "template" must be of type string + $variableContext['template'] = $variableContext['template'] ?? ''; + + $twigTemplate = $this->twigInstance->createTemplate($template); + $rendered = $twigTemplate->render($variableContext); + + // disable sandbox again if it has been disabled before + if (!$this->sandboxOverride && !$prevSandboxState) { + $this->sandboxExtensionInstance->disableSandbox(); + } + + return $rendered; } - } - - /** - * @inheritDoc - * - * twig loads a view like this (fixed:) - * frontend/view//.html.twig - * NOTE: extension .twig added by render() - */ - public function renderView(string $viewPath, $data = null) : string { - return $this->render('view/' . $viewPath, $data); - } - - /** - * @inheritDoc - * - * twig loads a template like this (fixed:) - * frontend/template//template.html.twig - * NOTE: extension .twig added by render() - */ - public function renderTemplate(string $templatePath, $data = null) : string { - return $this->render('template/' . $templatePath . '/template', $data); - } + /** + * {@inheritDoc} + * + * twig loads a view like this (fixed:) + * frontend/view//.html.twig + * NOTE: extension .twig added by render() + * @param string $viewPath + * @param object|array|null $data + * @return string + * @throws Throwable + */ + public function renderView(string $viewPath, object|array|null $data = null): string + { + return $this->render('view/' . $viewPath, $data); + } + /** + * {@inheritDoc} + * + * twig loads a template like this (fixed:) + * frontend/template//template.html.twig + * NOTE: extension .twig added by render() + * @param string $templatePath + * @param object|array|null $data + * @return string + * @throws Throwable + */ + public function renderTemplate(string $templatePath, object|array|null $data = null): string + { + return $this->render('template/' . $templatePath . '/template', $data); + } } diff --git a/backend/class/templateengine/twig/environment/core.php b/backend/class/templateengine/twig/environment/core.php index b0949d7..5d69b04 100644 --- a/backend/class/templateengine/twig/environment/core.php +++ b/backend/class/templateengine/twig/environment/core.php @@ -1,7 +1,10 @@ templateClassPrefixPrefix == null) { - $this->templateClassPrefixPrefix = '__'.$prefix.'_'; - } else { - throw new exception('EXCEPTION_CORE_UI_TEMPLATEENGINE_TWIG_ENVIRONMENT_CANNOT_CHANGE_TEMPLATE_CLASS_PREFIX_PREFIX', exception::$ERRORLEVEL_FATAL); + /** + * [setTemplateClassPrefixPrefix description] + * @param string $prefix [description] + * @throws exception + */ + public function setTemplateClassPrefixPrefix(string $prefix): void + { + if ($this->templateClassPrefixPrefix == null) { + $this->templateClassPrefixPrefix = '__' . $prefix . '_'; + } else { + throw new exception('EXCEPTION_CORE_UI_TEMPLATEENGINE_TWIG_ENVIRONMENT_CANNOT_CHANGE_TEMPLATE_CLASS_PREFIX_PREFIX', exception::$ERRORLEVEL_FATAL); + } } - } - /** - * @inheritDoc - */ - public function getTemplateClass($name, $index = null) - { - return ($this->templateClassPrefixPrefix ?? '').parent::getTemplateClass($name, $index); - } - - /** - * default template file suffix (e.g. .twig) - * @var [type] - */ - public $templateFileSuffix = ''; - - /** - * @inheritDoc - */ - public function loadTemplate($name, $index = null) - { - return parent::loadTemplate($name . $this->templateFileSuffix, $index); - } - -} \ No newline at end of file + /** + * {@inheritDoc} + * @param $name + * @param null $index + * @return string + * @throws LoaderError + */ + public function getTemplateClass($name, $index = null): string + { + return ($this->templateClassPrefixPrefix ?? '') . parent::getTemplateClass($name, $index); + } +} diff --git a/backend/class/templateengine/twig/extension/display.php b/backend/class/templateengine/twig/extension/display.php new file mode 100644 index 0000000..aba7a1e --- /dev/null +++ b/backend/class/templateengine/twig/extension/display.php @@ -0,0 +1,30 @@ +templateFileExtension = $templateFileExtension ?? $this->templateFileExtension; + } + + /** + * @inheritdoc + */ + public function getNodeVisitors(): array + { + return [new displayNodeVisitor($this->templateFileExtension)]; + } +} diff --git a/backend/class/templateengine/twig/extension/routing.php b/backend/class/templateengine/twig/extension/routing.php index 236bc50..6b7d2d1 100644 --- a/backend/class/templateengine/twig/extension/routing.php +++ b/backend/class/templateengine/twig/extension/routing.php @@ -1,8 +1,10 @@ generator = $generator; - $this->generator = new \codename\core\generator\urlGenerator; + $this->generator = new urlGenerator(); } + /** * Returns a list of functions to add to the existing list. * * @return array An array of functions */ - public function getFunctions() + public function getFunctions(): array { - return array( - new TwigFunction('url', array($this, 'getUrl'), array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))), - new TwigFunction('path', array($this, 'getPath'), array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))), - ); + return [ + new TwigFunction('url', [$this, 'getUrl'], ['is_safe_callback' => [$this, 'isUrlGenerationSafe']]), + new TwigFunction('path', [$this, 'getPath'], ['is_safe_callback' => [$this, 'isUrlGenerationSafe']]), + ]; } + /** * @param string $name - * @param array $parameters - * @param bool $relative + * @param array $parameters + * @param bool $relative * * @return string */ - public function getPath($name, $parameters = array(), $relative = false) + public function getPath(string $name, array $parameters = [], bool $relative = false): string { - // return $this->generator->generate($name, $parameters, $relative ? UrlGeneratorInterface::RELATIVE_PATH : UrlGeneratorInterface::ABSOLUTE_PATH); return $this->generator->generateFromRoute($name, $parameters, $relative ? urlGeneratorInterface::RELATIVE_PATH : urlGeneratorInterface::ABSOLUTE_PATH); } + /** * @param string $name - * @param array $parameters - * @param bool $schemeRelative + * @param array $parameters + * @param bool $schemeRelative * * @return string */ - public function getUrl($name, $parameters = array(), $schemeRelative = false) + public function getUrl(string $name, array $parameters = [], bool $schemeRelative = false): string { // return $this->generator->generate($name, $parameters, $schemeRelative ? UrlGeneratorInterface::NETWORK_PATH : UrlGeneratorInterface::ABSOLUTE_URL); return $this->generator->generateFromRoute($name, $parameters, $schemeRelative ? urlGeneratorInterface::NETWORK_PATH : urlGeneratorInterface::ABSOLUTE_URL); @@ -66,8 +69,8 @@ public function getUrl($name, $parameters = array(), $schemeRelative = false) * saving the unneeded automatic escaping for performance reasons. * * The URL generation process percent encodes non-alphanumeric characters. So there is no risk - * that malicious/invalid characters are part of the URL. The only character within an URL that - * must be escaped in html is the ampersand ("&") which separates query params. So we cannot mark + * that malicious/invalid characters are part of the URL. The only character within a URL that + * must be escaped in HTML is the ampersand ("&") which separates query params. So we cannot mark * the URL generation as always safe, but only when we are sure there won't be multiple query * params. This is the case when there are none or only one constant parameter given. * E.g. we know beforehand this will be safe: @@ -85,24 +88,25 @@ public function getUrl($name, $parameters = array(), $schemeRelative = false) * * @final since version 3.4 */ - public function isUrlGenerationSafe(Node $argsNode) + public function isUrlGenerationSafe(Node $argsNode): array { // support named arguments $paramsNode = $argsNode->hasNode('parameters') ? $argsNode->getNode('parameters') : ( - $argsNode->hasNode(1) ? $argsNode->getNode(1) : null + $argsNode->hasNode(1) ? $argsNode->getNode(1) : null ); if (null === $paramsNode || $paramsNode instanceof ArrayExpression && count($paramsNode) <= 2 && - (!$paramsNode->hasNode(1) || $paramsNode->getNode(1) instanceof ConstantExpression) + (!$paramsNode->hasNode(1) || $paramsNode->getNode(1) instanceof ConstantExpression) ) { - return array('html'); + return ['html']; } - return array(); + return []; } + /** - * {@inheritdoc} + * @return string */ - public function getName() + public function getName(): string { return 'routing'; } -} \ No newline at end of file +} diff --git a/backend/class/templateengine/twig/loader/filesystem.php b/backend/class/templateengine/twig/loader/filesystem.php deleted file mode 100644 index 8d671a7..0000000 --- a/backend/class/templateengine/twig/loader/filesystem.php +++ /dev/null @@ -1,22 +0,0 @@ -templateFileSuffix, $throw); - } -} diff --git a/backend/class/templateengine/twig/node/displayNode.php b/backend/class/templateengine/twig/node/displayNode.php new file mode 100644 index 0000000..5768e15 --- /dev/null +++ b/backend/class/templateengine/twig/node/displayNode.php @@ -0,0 +1,45 @@ +templateFileExtension = $templateFileExtension ?? $this->templateFileExtension; + } + + /** + * @inheritdoc + */ + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write( + ' + protected function loadTemplate($template, $templateName = null, $line = null, $index = null): parent|TemplateWrapper + { + $template .= "' . $this->templateFileExtension . '"; + if (str_starts_with($template, \'./\') && $templateName) { + $template = dirname($templateName) . ltrim($template, \'.\'); + } + return parent::loadTemplate($template, $templateName, $line, $index); + } + ' + ); + } +} diff --git a/backend/class/templateengine/twig/nodevisitor/displayNodeVisitor.php b/backend/class/templateengine/twig/nodevisitor/displayNodeVisitor.php new file mode 100644 index 0000000..050f43a --- /dev/null +++ b/backend/class/templateengine/twig/nodevisitor/displayNodeVisitor.php @@ -0,0 +1,61 @@ + + */ +final class displayNodeVisitor implements NodeVisitorInterface +{ + /** + * @var string + */ + protected string $templateFileExtension = ''; + + /** + * @param string|null $templateFileExtension + */ + public function __construct(?string $templateFileExtension = '') + { + $this->templateFileExtension = $templateFileExtension ?? $this->templateFileExtension; + } + + /** + * @inheritdoc + */ + public function enterNode(Node $node, Environment $env): Node + { + return $node; + } + + /** + * @inheritdoc + */ + public function leaveNode(Node $node, Environment $env): ?Node + { + if ($node instanceof ModuleNode) { + $node->setNode( + 'class_end', + new Node([ + new displayNode($this->templateFileExtension), + $node->getNode('class_end'), + ]) + ); + } + return $node; + } + + /** + * @inheritdoc + */ + public function getPriority(): int + { + return 0; + } +} diff --git a/backend/class/templateengine/twig/template/baseTemplate.php b/backend/class/templateengine/twig/template/baseTemplate.php deleted file mode 100644 index 1e9c792..0000000 --- a/backend/class/templateengine/twig/template/baseTemplate.php +++ /dev/null @@ -1,36 +0,0 @@ - - {% for column in data.header %} - {{column}} - {% endfor %} + {% for column in data.header %} + {{ column }} + {% endfor %} {% for row in data.rows %} - {% for column in data.header %} - {{ row[column] }} - {% endfor %} + {% for column in data.header %} + {{ row[column] }} + {% endfor %} {% endfor %} diff --git a/frontend/field/compact/button.php b/frontend/field/compact/button.php index 42bacdc..d4621ad 100644 --- a/frontend/field/compact/button.php +++ b/frontend/field/compact/button.php @@ -1,13 +1,18 @@ - +
- -
- -
+ +
+ +
diff --git a/frontend/field/compact/checkbox.php b/frontend/field/compact/checkbox.php index 75da624..c500266 100644 --- a/frontend/field/compact/checkbox.php +++ b/frontend/field/compact/checkbox.php @@ -1,23 +1,41 @@ - +
- -
- - - - /> -
-
+ +
+ + + + /> +
+
diff --git a/frontend/field/compact/date.php b/frontend/field/compact/date.php index 8e67002..a16c384 100644 --- a/frontend/field/compact/date.php +++ b/frontend/field/compact/date.php @@ -1,35 +1,49 @@ - +
- -
- - - /> -
-
+ +
+ + + /> +
+
- diff --git a/frontend/field/compact/file.php b/frontend/field/compact/file.php index 7dfe12c..bd22960 100644 --- a/frontend/field/compact/file.php +++ b/frontend/field/compact/file.php @@ -1,28 +1,46 @@ - +
- -
-
- - - /> -
-
+ +
+
+ + + />
+
+
diff --git a/frontend/field/compact/hidden.php b/frontend/field/compact/hidden.php index 5185914..c8ec6dc 100644 --- a/frontend/field/compact/hidden.php +++ b/frontend/field/compact/hidden.php @@ -1,7 +1,12 @@ - + diff --git a/frontend/field/compact/input.php b/frontend/field/compact/input.php index 0e05d96..5e56d60 100644 --- a/frontend/field/compact/input.php +++ b/frontend/field/compact/input.php @@ -1,25 +1,37 @@ - +
- -
- - /> -
-
+ +
+ + /> +
+
diff --git a/frontend/field/compact/multicheckbox.php b/frontend/field/compact/multicheckbox.php index 30707ae..48e0880 100644 --- a/frontend/field/compact/multicheckbox.php +++ b/frontend/field/compact/multicheckbox.php @@ -1,45 +1,61 @@ - +
- -
+ +
- + +$counter = 1; +foreach ($data['field_elements'] as $element) { ?> - - - /> + if ($data['field_value'] != null && !is_array($data['field_value']) && $element[$data['field_valuefield']] == $data['field_value']) { + echo 'checked'; + } + if (is_array($data['field_value']) && in_array($element[$data['field_valuefield']], $data['field_value'])) { + echo 'checked'; + } + ?> + + /> - -
- + +
+ - + - -
+
diff --git a/frontend/field/compact/password.php b/frontend/field/compact/password.php index ddecac9..06e62e2 100644 --- a/frontend/field/compact/password.php +++ b/frontend/field/compact/password.php @@ -1,26 +1,38 @@ - +
- -
- - /> -
-
+ +
+ + /> +
+
diff --git a/frontend/field/compact/radio.php b/frontend/field/compact/radio.php index d1b602a..a4baa32 100644 --- a/frontend/field/compact/radio.php +++ b/frontend/field/compact/radio.php @@ -1,34 +1,52 @@ - +
- -
+ +
- -
- - value="" - > - -
- + +
+ + value="" + > + +
+ -
+
-
+
diff --git a/frontend/field/compact/select.php b/frontend/field/compact/select.php index 86e4d59..769b266 100644 --- a/frontend/field/compact/select.php +++ b/frontend/field/compact/select.php @@ -1,61 +1,94 @@ -
- -
- - - - - - + + + -
- -
+ } ?> + + + + + + +
+ +
\ No newline at end of file diff --git a/frontend/field/compact/structure.php b/frontend/field/compact/structure.php index 62b70d3..8229c78 100644 --- a/frontend/field/compact/structure.php +++ b/frontend/field/compact/structure.php @@ -1,76 +1,87 @@ -requireResource('js', '/assets/plugins/jsoneditor/jsoneditor.min.js'); -app::getResponse()->requireResource('css', '/assets/plugins/jsoneditor/jsoneditor.min.css'); -app::getResponse()->requireResource('js', '/assets/plugins/jsoneditor/jsoneditor.min.js'); -?> requireResource('js', '/assets/plugins/jsoneditor/jsoneditor.min.js'); +$response->requireResource('css', '/assets/plugins/jsoneditor/jsoneditor.min.css'); +$response->requireResource('js', '/assets/plugins/jsoneditor/jsoneditor.min.js'); ?>
- + -
+
-
+
diff --git a/frontend/field/compact/structure_address.php b/frontend/field/compact/structure_address.php index fcbd7a7..d9b97f9 100644 --- a/frontend/field/compact/structure_address.php +++ b/frontend/field/compact/structure_address.php @@ -1,17 +1,31 @@ - - - - 'input','field_name' => $data['field_id'] . '__POSTALCODE','field_title' => app::getTranslate()->translate('DATAFIELD.POSTALCODE'),'field_required' => true, 'field_readonly' => $data['field_readonly'], 'field_value' => $data['field_value']['postalcode'])))->output();?> - 'input','field_name' => $data['field_id'] . '__CITY','field_title' => app::getTranslate()->translate('DATAFIELD.CITY'),'field_required' => true, 'field_readonly' => $data['field_readonly'], 'field_value' => $data['field_value']['city'])))->output();?> - 'input','field_name' => $data['field_id'] . '__STREET','field_title' => app::getTranslate()->translate('DATAFIELD.STREET'),'field_required' => true, 'field_readonly' => $data['field_readonly'], 'field_value' => $data['field_value']['street'])))->output();?> - 'input','field_name' => $data['field_id'] . '__NUMBER','field_title' => app::getTranslate()->translate('DATAFIELD.NUMBER'),'field_required' => true,'field_readonly' => $data['field_readonly'], 'field_value' => $data['field_value']['number'])))->output();?> + + + 'input', 'field_name' => $data['field_id'] . '__POSTALCODE', 'field_title' => app::getTranslate()->translate('DATAFIELD.POSTALCODE'), 'field_required' => true, 'field_readonly' => $data['field_readonly'], 'field_value' => $data['field_value']['postalcode']]))->output(); ?> + 'input', 'field_name' => $data['field_id'] . '__CITY', 'field_title' => app::getTranslate()->translate('DATAFIELD.CITY'), 'field_required' => true, 'field_readonly' => $data['field_readonly'], 'field_value' => $data['field_value']['city']]))->output(); ?> + 'input', 'field_name' => $data['field_id'] . '__STREET', 'field_title' => app::getTranslate()->translate('DATAFIELD.STREET'), 'field_required' => true, 'field_readonly' => $data['field_readonly'], 'field_value' => $data['field_value']['street']]))->output(); ?> + 'input', 'field_name' => $data['field_id'] . '__NUMBER', 'field_title' => app::getTranslate()->translate('DATAFIELD.NUMBER'), 'field_required' => true, 'field_readonly' => $data['field_readonly'], 'field_value' => $data['field_value']['number']]))->output(); ?> diff --git a/frontend/field/compact/submit.php b/frontend/field/compact/submit.php index 6b9182e..41590b6 100644 --- a/frontend/field/compact/submit.php +++ b/frontend/field/compact/submit.php @@ -1,12 +1,17 @@ - +
- -
- -
+ +
+ +
diff --git a/frontend/field/compact/tags.php b/frontend/field/compact/tags.php index 5fc02e8..b00db96 100644 --- a/frontend/field/compact/tags.php +++ b/frontend/field/compact/tags.php @@ -1,24 +1,36 @@ - +
- -
+ +
-
-
+ id="" + name="" + type="text" + class="tags form-control " + title="" + data-datatype="" + data-placeholder="" + data-description="" + data-prompt-position="topLeft" + + > +
+
diff --git a/frontend/field/compact/textarea.php b/frontend/field/compact/textarea.php index b5394af..b9e1daf 100644 --- a/frontend/field/compact/textarea.php +++ b/frontend/field/compact/textarea.php @@ -1,24 +1,36 @@ - +
- -
+ +
-
-
+ id="" + name="" + type="text" + class="validate[] form-control " + title="" + data-datatype="" + data-placeholder="" + data-description="" + data-prompt-position="topLeft" + + > +
+
diff --git a/frontend/field/compact/timestamp.php b/frontend/field/compact/timestamp.php index 2a29bad..452f0dc 100644 --- a/frontend/field/compact/timestamp.php +++ b/frontend/field/compact/timestamp.php @@ -1,48 +1,73 @@ -requireResource('css', 'assets/plugins/datetimepicker/jquery.datetimepicker.min.css'); -app::getResponse()->requireResource('js', 'assets/plugins/datetimepicker/jquery.datetimepicker.full.js'); +requireResource('css', 'assets/plugins/datetimepicker/jquery.datetimepicker.min.css'); +$response->requireResource('js', 'assets/plugins/datetimepicker/jquery.datetimepicker.full.js'); ?>
- -
- ="" - type="datetime-local" - class="validate[] form-control datetimepickers" - title="" - data-datatype="" - data-placeholder="" - data-description="" - data-prompt-position="topLeft" - - /> - - - -
-
+ color: #c00;" + > + +
+
diff --git a/frontend/field/default/button.twig b/frontend/field/default/button.twig index 1cbfa82..627bc7e 100644 --- a/frontend/field/default/button.twig +++ b/frontend/field/default/button.twig @@ -1,12 +1,12 @@
- -
- -
+ +
+ +
diff --git a/frontend/field/default/hidden.php b/frontend/field/default/hidden.php index 5fc4bdf..c326b71 100644 --- a/frontend/field/default/hidden.php +++ b/frontend/field/default/hidden.php @@ -1,7 +1,12 @@ - + diff --git a/frontend/field/default/hidden.twig b/frontend/field/default/hidden.twig index d2a62db..76461d3 100644 --- a/frontend/field/default/hidden.twig +++ b/frontend/field/default/hidden.twig @@ -1,6 +1,6 @@ diff --git a/frontend/field/default/input.php b/frontend/field/default/input.php index b45c6fc..855d035 100644 --- a/frontend/field/default/input.php +++ b/frontend/field/default/input.php @@ -1,25 +1,37 @@ - +
- -
- - /> -
-
+ +
+ + /> +
+
diff --git a/frontend/field/default/input.twig b/frontend/field/default/input.twig index 6bd162b..25cc5da 100644 --- a/frontend/field/default/input.twig +++ b/frontend/field/default/input.twig @@ -1,24 +1,26 @@
- -
- -
-
+ +
+ +
+
diff --git a/frontend/field/default/submit.php b/frontend/field/default/submit.php index 29a7887..52070ee 100644 --- a/frontend/field/default/submit.php +++ b/frontend/field/default/submit.php @@ -1,12 +1,16 @@ - +
- -
- -
+ +
+ +
diff --git a/frontend/field/default/submit.twig b/frontend/field/default/submit.twig index d1c4ed5..2b779ad 100644 --- a/frontend/field/default/submit.twig +++ b/frontend/field/default/submit.twig @@ -1,11 +1,11 @@
- -
- -
+ +
+ +
diff --git a/frontend/form/compact/form.php b/frontend/form/compact/form.php index f48b955..08f22d3 100644 --- a/frontend/form/compact/form.php +++ b/frontend/form/compact/form.php @@ -1,166 +1,173 @@ - []]; + // app::getResponse()->requireResource('js', '/assets/plugins/jquery.loadinganim/jquery.loadinganim.js'); // app::getResponse()->requireResource('css', '/assets/plugins/jquery.loadinganim/jquery.loadinganim.css'); app::requireAsset('requirejs', ['jquery']); ?> -
+
-
-
- fieldsets as $fieldset) { - echo $fieldset->output(); - } - } - foreach ($data->fields as $field) { - echo $field->output(); - } - ?> -
+
+
+ fieldsets as $fieldset) { + echo $fieldset->output(); + } + } + foreach ($data->fields as $field) { + echo $field->output(); + } +?> +
-config['form_text_requiredfields']) && $data->config['form_text_requiredfields'] != null) { ?> -
- -
- +config['form_text_requiredfields']) && $data->config['form_text_requiredfields'] != null) { ?> +
+ +
+
config['form_scripts'])) { - if(isset($data->config['form_scripts']['path'])) { - foreach($data->config['form_scripts']['path'] as $scriptPath) { - echo(app::parseFile(app::getInheritedPath($scriptPath), $data)); - } +// additional scripts paths defined +if (isset($data->config['form_scripts'])) { + if (isset($data->config['form_scripts']['path'])) { + foreach ($data->config['form_scripts']['path'] as $scriptPath) { + echo(app::parseFile(app::getInheritedPath($scriptPath), $data)); + } } - if(isset($data->config['form_scripts']['code'])) { - foreach($data->config['form_scripts']['code'] as $code) { - echo($code); - } + if (isset($data->config['form_scripts']['code'])) { + foreach ($data->config['form_scripts']['code'] as $code) { + echo($code); + } } - } +} ?> diff --git a/frontend/form/default/form.php b/frontend/form/default/form.php index 4eb95c0..2104696 100644 --- a/frontend/form/default/form.php +++ b/frontend/form/default/form.php @@ -1,41 +1,50 @@ - -
-
+config['form_id']?>" class="hcForm"> -
- fieldsets as $fieldset) { - echo $fieldset->output(); - } - } - foreach ($data->fields as $field) { - echo $field->output(); - } - ?> -
+namespace codename\core\ui; + +$data = $data ?? (object)['config' => []]; +?> +
-config['form_text_requiredfields']) && $data->config['form_text_requiredfields'] != null) { ?> -
- +
+
+ fieldsets as $fieldset) { + echo $fieldset->output(); + } + } + foreach ($data->fields as $field) { + echo $field->output(); + } +?> +
- + +config['form_text_requiredfields']) && $data->config['form_text_requiredfields'] != null) { ?> +
+ +
+
config['form_scripts'])) { - if(isset($data->config['form_scripts']['path'])) { - foreach($data->config['form_scripts']['path'] as $scriptPath) { - echo(app::parseFile(app::getInheritedPath($scriptPath), $data)); - } +// additional scripts paths defined +if (isset($data->config['form_scripts'])) { + if (isset($data->config['form_scripts']['path'])) { + foreach ($data->config['form_scripts']['path'] as $scriptPath) { + echo(app::parseFile(app::getInheritedPath($scriptPath), $data)); + } } - if(isset($data->config['form_scripts']['code'])) { - foreach($data->config['form_scripts']['code'] as $code) { - echo($code); - } + if (isset($data->config['form_scripts']['code'])) { + foreach ($data->config['form_scripts']['code'] as $code) { + echo($code); + } } - } +} ?> diff --git a/frontend/form/default/form.twig b/frontend/form/default/form.twig index 8a86a77..581558c 100644 --- a/frontend/form/default/form.twig +++ b/frontend/form/default/form.twig @@ -3,19 +3,20 @@
- {% for fieldset in data.fieldsets %} - {{ fieldset.output()|raw }} - {% endfor %} - {% for field in data.fields %} - {{ field.output()|raw }} - {% endfor %} + {% for fieldset in data.fieldsets %} + {{ fieldset.output()|raw }} + {% endfor %} + {% for field in data.fields %} + {{ field.output()|raw }} + {% endfor %}
- {% if data.config.form_text_requiredfields %}
- +
{% endif %} @@ -28,8 +29,8 @@ {% endfor %} {% endif %} {% if data.config.form_scripts.code %} - {% for code in data.config.form_scripts.code %} - {{ code|raw }} - {% endfor %} + {% for code in data.config.form_scripts.code %} + {{ code|raw }} + {% endfor %} {% endif %} {% endif %} diff --git a/frontend/template/basic/template.cli.php b/frontend/template/basic/template.cli.php index c9f1f69..bbfaded 100644 --- a/frontend/template/basic/template.cli.php +++ b/frontend/template/basic/template.cli.php @@ -1,27 +1,47 @@ - + - - - <?= app::getResponse()->getData('title') ?> - getResources('js') as $js) { ?> - - - getResources('script') as $script) { ?> - - - getResources('css') as $css) { ?> - - - getResources('style') as $style) { ?> - - - getResources('head') as $head) { ?> + + + <?= $response->getData('title') ?> + getResources('js') as $js) { ?> + + + getResources('script') as $script) { ?> + + + getResources('css') as $css) { ?> + + + getResources('style') as $style) { ?> + + + getResources('head') as $head) { ?> - - - - getData('content') ?> - + + + + getData('content') ?> + diff --git a/frontend/template/basic/template.php b/frontend/template/basic/template.php index c9f1f69..2c2090c 100644 --- a/frontend/template/basic/template.php +++ b/frontend/template/basic/template.php @@ -1,27 +1,47 @@ - + - - - <?= app::getResponse()->getData('title') ?> - getResources('js') as $js) { ?> - - - getResources('script') as $script) { ?> - - - getResources('css') as $css) { ?> - - - getResources('style') as $style) { ?> - - - getResources('head') as $head) { ?> + + + <?= $response->getData('title') ?> + getResources('js') as $js) { ?> + + + getResources('script') as $script) { ?> + + + getResources('css') as $css) { ?> + + + getResources('style') as $style) { ?> + + + getResources('head') as $head) { ?> - - - - getData('content') ?> - + + + + getData('content') ?> + diff --git a/frontend/template/basic/template.twig b/frontend/template/basic/template.twig index 1277327..652bd76 100644 --- a/frontend/template/basic/template.twig +++ b/frontend/template/basic/template.twig @@ -1,28 +1,28 @@ - - - {{ response.getData('title') }} + + + {{ response.getData('title') }} - {% for js in response.getResources('js') %} - - {% endfor %} - {% for script in response.getResources('script') %} - - {% endfor %} - {% for css in response.getResources('css') %} - - {% endfor %} - {% for style in response.getResources('style') %} - - {% endfor %} - {% for head in response.getResources('head') %} - {{ head | raw }} - {% endfor %} + {% for js in response.getResources('js') %} + + {% endfor %} + {% for script in response.getResources('script') %} + + {% endfor %} + {% for css in response.getResources('css') %} + + {% endfor %} + {% for style in response.getResources('style') %} + + {% endfor %} + {% for head in response.getResources('head') %} + {{ head | raw }} + {% endfor %} - - - {{ response.getData('content')|raw }} - + + + {{ response.getData('content')|raw }} + diff --git a/frontend/template/blank/template.cli.php b/frontend/template/blank/template.cli.php index a20801a..171dc59 100644 --- a/frontend/template/blank/template.cli.php +++ b/frontend/template/blank/template.cli.php @@ -1,2 +1,6 @@ - -getData('content')?> + +getData('content') ?> diff --git a/frontend/template/blank/template.php b/frontend/template/blank/template.php index a20801a..171dc59 100644 --- a/frontend/template/blank/template.php +++ b/frontend/template/blank/template.php @@ -1,2 +1,6 @@ - -getData('content')?> + +getData('content') ?> diff --git a/frontend/view/crud/crud_create.php b/frontend/view/crud/crud_create.php index 3414669..ed9c42c 100644 --- a/frontend/view/crud/crud_create.php +++ b/frontend/view/crud/crud_create.php @@ -1,10 +1,14 @@ - -getData('form')?> +getData('form') ?> diff --git a/frontend/view/crud/crud_delete.php b/frontend/view/crud/crud_delete.php index 6d554a9..6468df3 100644 --- a/frontend/view/crud/crud_delete.php +++ b/frontend/view/crud/crud_delete.php @@ -1,24 +1,31 @@ - +
-
-
-
-
-
-
- - - -
+
+
+
+
+
+
+ + + + + +
+
diff --git a/frontend/view/crud/crud_edit.php b/frontend/view/crud/crud_edit.php index 3414669..ed9c42c 100644 --- a/frontend/view/crud/crud_edit.php +++ b/frontend/view/crud/crud_edit.php @@ -1,10 +1,14 @@ - -getData('form')?> +getData('form') ?> diff --git a/frontend/view/crud/crud_list.php b/frontend/view/crud/crud_list.php index 1680047..d28cd1e 100644 --- a/frontend/view/crud/crud_list.php +++ b/frontend/view/crud/crud_list.php @@ -1,11 +1,19 @@ -requireResource('js', '/assets/plugins/jquery.bsmodal/jquery.bsmodal.js'); @@ -28,7 +36,7 @@ $crud_filter_identifier = app::getResponse()->getData('crud_filter_identifier'); // filters -$filters_used = app::getResponse()->getData('filters_used'); +$filters_used = app::getResponse()->getData('filters_used'); $filters_unused = app::getResponse()->getData('filters'); // enablers @@ -46,468 +54,576 @@ $enable_export = app::getResponse()->getData('enable_export'); // pagination $crud_pagination_pages = app::getResponse()->getData('crud_pagination_pages'); -$crud_pagination_page = app::getResponse()->getData('crud_pagination_page'); +$crud_pagination_page = app::getResponse()->getData('crud_pagination_page'); $crud_pagination_limit = app::getResponse()->getData('crud_pagination_limit'); $crud_pagination_count = app::getResponse()->getData('crud_pagination_count'); -$crud_pagination_display=count($data_rows); +$crud_pagination_display = count($data_rows); // basefilter $url_baseurl = app::getResponse()->getData('url_baseurl'); ?> - -
- -
- - $value) { ?> - + +
+ +
+ + $value) { ?> +
- -
- $value['context'] ?? app::getRequest()->getData('context'), - 'view' => $value['view'], - ); + $value['context'] ?? app::getRequest()->getData('context'), + 'view' => $value['view'], + ]; + + if (isset($value['parameter'])) { + if (is_array($value['parameter'])) { + foreach ($value['parameter'] as $param) { + $requestParams[$param] = app::getRequest()->getData($param); + } + } else { + $requestParams[$value['parameter']] = app::getRequest()->getData($value['parameter']); + } + } - if(isset($value['parameter'])) { - if(is_array($value['parameter'])) { - foreach($value['parameter'] as $param) { - $requestParams[$param] = app::getRequest()->getData($param); - } - } else { - $requestParams[$value['parameter']] = app::getRequest()->getData($value['parameter']); + if (isset($value['params'])) { + $requestParams = array_merge($requestParams, $value['params']); } - } - - if(isset($value['params'])) { - $requestParams = array_merge($requestParams, $value['params']); - } - ?> - translate($value['translationkey']) : app::getTranslate()->translate("BUTTON.BTN_" . $key)?> - - - - -
- - -
- -
- - -
- + ?> + + translate($value['translationkey']) : app::getTranslate()->translate("BUTTON.BTN_" . $key) ?> + + + + +
+ -
- -
- - -
- - - - - - -
- - - -
- +
+ +
+ + +
+ + +
+ +
+ + + + + + + + + +
+ + + +
+
- not an array: display_availablefields"); - } - if(!is_array($display_selectedfields)) { - echo("
not an array: display_selectedfields"); - } + not an array: display_availablefields"); + } + if (!is_array($display_selectedfields)) { + echo("
not an array: display_selectedfields"); + } } ?> -
- - - -
- +
+ + + +
+ +
+ : +
+ + +
+ + + $filter) { ?> +
- :
- - -
- - - $filter) { ?> - -
- translate('DATAFIELD.' . $field)?>:
- - > + $value) { ?> + - - - - - - - - - -
- - -
- + } elseif (app::getRequest()->getData($crud_filter_identifier)[$field] == $key) { + echo('selected="selected"'); + } ?>> + + + + + + + + + + + +
+ + +
+ -
- - - - +
+ + + + - +
- - - - - - - - - - - - - - - - - > - - - - - - - + + + + + + + +
translate('DATAFIELD.' . $field)?>
- getPrimarykey()] != null) { ?> - - - - getPrimarykey()] != null) { ?> -
- -
+ + Zeige von + +
+ 1) { ?> + + + + + + +
+
+ +
- diff --git a/frontend/view/crud/crud_success.php b/frontend/view/crud/crud_success.php index c6ed39f..3273a76 100644 --- a/frontend/view/crud/crud_success.php +++ b/frontend/view/crud/crud_success.php @@ -1,20 +1,24 @@ - + diff --git a/frontend/view/crud/save_error.php b/frontend/view/crud/save_error.php index ee1aa60..f816848 100644 --- a/frontend/view/crud/save_error.php +++ b/frontend/view/crud/save_error.php @@ -1,16 +1,23 @@ - + diff --git a/phpcs.xml b/phpcs.xml index 73b533d..8644693 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,21 +1,21 @@ - The default CoreFramework coding standard + The default CoreFramework coding standard - - - + + + - - + + - - + + \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 8335665..fa3d97c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,11 +1,17 @@ - - + + backend/class/ - + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..a1b8601 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/tests/appTest.php b/tests/appTest.php index dec5118..068aa72 100644 --- a/tests/appTest.php +++ b/tests/appTest.php @@ -1,63 +1,105 @@ expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_CORE_UI_APP_ILLEGAL_CALL'); + + new app(); + } + + /** + * [testRun description] + * @throws ReflectionException + * @throws exception + */ + public function testRun(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_CORE_UI_APP_ILLEGAL_CALL'); + + (new appRun())->run(); + } + + /** + * [testUrlGenerator description] + */ + public function testUrlGenerator(): void + { + $urlGenerator = app::getUrlGenerator(); + static::assertEquals((new urlGenerator()), $urlGenerator); + + $restUrlGenerator = new restUrlGenerator(); + app::setUrlGenerator($restUrlGenerator); - /** - * @inheritDoc - */ - protected function setUp(): void - { - parent::setUp(); - - $app = static::createApp(); - overrideableApp::__injectApp([ - 'vendor' => 'codename', - 'app' => 'core-ui', - 'namespace' => '\\codename\\core\\ui' - ]); - $app->getAppstack(); - } - - - /** - * [testConstruct description] - */ - public function testConstruct(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_CORE_UI_APP_ILLEGAL_CALL'); - - $app = new \codename\core\ui\app(); - } - - /** - * [testRun description] - */ - public function testRun(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_CORE_UI_APP_ILLEGAL_CALL'); - - $app = \codename\core\ui\app::run(); - } - - /** - * [testUrlGenerator description] - */ - public function testUrlGenerator(): void { - $urlGenerator = \codename\core\ui\app::getUrlGenerator(); - $this->assertEquals((new \codename\core\generator\urlGenerator()), $urlGenerator); - - $restUrlGenerator = new \codename\core\generator\restUrlGenerator(); - \codename\core\ui\app::setUrlGenerator($restUrlGenerator); - - $getUrlGenerator = \codename\core\ui\app::getUrlGenerator(); - $this->assertEquals($restUrlGenerator, $getUrlGenerator); - - } + $getUrlGenerator = app::getUrlGenerator(); + static::assertEquals($restUrlGenerator, $getUrlGenerator); + } + + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws ErrorException + * @throws Throwable + * @throws compileErrorException + * @throws coreErrorException + * @throws coreWarningException + * @throws parseException + * @throws recoverableErrorException + * @throws strictException + * @throws userErrorException + * @throws userWarningException + * @throws warningException + * @throws exception + */ + protected function setUp(): void + { + parent::setUp(); + + $app = static::createApp(); + overrideableApp::__injectApp([ + 'vendor' => 'codename', + 'app' => 'core-ui', + 'namespace' => '\\codename\\core\\ui', + ]); + $app::getAppstack(); + } +} + +class appRun extends app +{ + /** + * + */ + public function __construct() + { + } } diff --git a/tests/autoload.php b/tests/autoload.php index d581b77..30173ba 100644 --- a/tests/autoload.php +++ b/tests/autoload.php @@ -4,11 +4,11 @@ * This is a per-project autoloading file * For initializing the local project and enabling it for development purposes * - * you need to build up your fullstack autoloading structure + * You need to build up your fullstack autoloading structure * using composer install / composer update - * e.g. for /composer.json + * e.g., for /composer.json * - * and you need to build a local composer classmap + * And you need to build a local composer classmap * that enables the usage of composer's 'autoload-dev' setting * just for this project * @@ -17,53 +17,56 @@ */ // Default fixed environment for unit tests -define('CORE_ENVIRONMENT', 'test'); +use codename\core\app; +use codename\core\test\overrideableApp; + +const CORE_ENVIRONMENT = 'test'; // cross-project autoloader -$globalBootstrap = realpath(__DIR__.'/../../../../bootstrap-cli.php'); -if(file_exists($globalBootstrap)) { - echo("Including autoloader at " . $globalBootstrap . chr(10) ); - require_once $globalBootstrap; +$globalBootstrap = realpath(__DIR__ . '/../../../../bootstrap-cli.php'); +if (file_exists($globalBootstrap)) { + echo("Including autoloader at " . $globalBootstrap . chr(10)); + require_once $globalBootstrap; } else { - // die("ERROR: No global bootstrap.cli.php found. You might want to initialize your cross-project autoloader using the root composer.json first." . chr(10) ); + die("ERROR: No global bootstrap.cli.php found. You might want to initialize your cross-project autoloader using the root composer.json first." . chr(10)); } // local autoloader -$localAutoload = realpath(__DIR__.'/../vendor/autoload.php'); -if(file_exists($localAutoload)) { - echo("Including autoloader at " . $localAutoload . chr(10) ); - require_once $localAutoload; +$localAutoload = realpath(__DIR__ . '/../vendor/autoload.php'); +if (file_exists($localAutoload)) { + echo("Including autoloader at " . $localAutoload . chr(10)); + require_once $localAutoload; } else { - // die("ERROR: No local vendor/autoloader.php found. Please call \"composer dump-autoload\" in this directory." . chr(10) ); + die("ERROR: No local vendor/autoloader.php found. Please call \"composer dump-autoload --dev\" in this directory." . chr(10)); } // // This allows having only a local autoloader and no global one -// (e.g. single-project unit testing) +// (e.g., single-project unit testing) // -if(!file_exists($globalBootstrap) && !file_exists($localAutoload)){ - die("ERROR: No global bootstrap.cli.php or local vendor/autoloader.php found. You might want to initialize your cross-project or single-project autoloader first." . chr(10) ); +if (!file_exists($globalBootstrap) && !file_exists($localAutoload)) { + die("ERROR: No global bootstrap.cli.php or local vendor/autoloader.php found. You might want to initialize your cross-project or single-project autoloader first." . chr(10)); } -if(!$globalBootstrap) { - // Fallback to this project's vendor dir (and add a slash at the end - because realpath doesn't add it) - DEFINE("CORE_VENDORDIR", realpath(DIRNAME(__FILE__) . '/../vendor/').'/'); +if (!$globalBootstrap) { + // Fallback to this project's vendor dir (and add a slash at the end - because realpath doesn't add it) + define("CORE_VENDORDIR", realpath(dirname(__FILE__) . '/../vendor/') . '/'); } // Explicitly reset any appdata left // or implicitly re-init base data. -\codename\core\test\overrideableApp::reset(); +overrideableApp::reset(); // -// Special quirk for single-project unit testing +// Special quirk for single-project unit testing, // We need to override the homedir for this app // as the framework itself assumes it resides in composer's vendor dir // // Additionally, we need to do this every time the appstack gets initialized in the tests // and only if this app is used, somehow. // -\codename\core\app::getHook()->add(\codename\core\app::EVENT_APP_APPSTACK_AVAILABLE, function() { - \codename\core\test\overrideableApp::__modifyAppstackEntry('codename', 'core-ui', [ - 'homedir' => realpath(__DIR__.'/../'), // One dir up (project root) - ]); +app::getHook()->add(app::EVENT_APP_APPSTACK_AVAILABLE, function () { + overrideableApp::__modifyAppstackEntry('codename', 'core-ui', [ + 'homedir' => realpath(__DIR__ . '/../'), // One dir up (project root) + ]); }); diff --git a/tests/crud/config/crud/crudtest_testmodel.json b/tests/crud/config/crud/crudtest_testmodel.json index 52ed5fd..0353d21 100644 --- a/tests/crud/config/crud/crudtest_testmodel.json +++ b/tests/crud/config/crud/crudtest_testmodel.json @@ -3,43 +3,43 @@ "pagination": { "limit": 5 }, - "displayFieldSelection" : true, - "customized_fields" : [ + "displayFieldSelection": true, + "customized_fields": [ "testmodel_testmodeljoin_id" ], - "visibleFields" : [ + "visibleFields": [ "testmodel_text", "testmodel_testmodeljoin_id" ], "visibleFilters": { - "testmodel_text" : { - "wildcard" : true + "testmodel_text": { + "wildcard": true } - }, - "children" : [ + }, + "children": [ "testmodel_testmodeljoin", "testmodel_testmodelcollection" ], - "children_config" : { - "testmodel_testmodeljoin" : { - "crud" : "crudtest_testmodeljoin", - "form" : "testmodeljoin" + "children_config": { + "testmodel_testmodeljoin": { + "crud": "crudtest_testmodeljoin", + "form": "testmodeljoin" } }, - "disabled" : [ + "disabled": [ "testmodel_unique_single", "testmodel_unique_multi1", "testmodel_unique_multi2" ], - "required" : [ + "required": [ "testmodel_text" ], - "readonly" : [ + "readonly": [ "testmodel_id" ], - "order" : { + "order": { }, - "action" : { - "crud_edit" : "example" + "action": { + "crud_edit": "example" } } diff --git a/tests/crud/config/crud/crudtest_testmodel_crudlistconfig.json b/tests/crud/config/crud/crudtest_testmodel_crudlistconfig.json index 7d30fdb..f24abca 100644 --- a/tests/crud/config/crud/crudtest_testmodel_crudlistconfig.json +++ b/tests/crud/config/crud/crudtest_testmodel_crudlistconfig.json @@ -2,64 +2,64 @@ "pagination": { "limit": 5 }, - "import" : { - "_security" : { - "group" : "group_true" + "import": { + "_security": { + "group": "group_true" } }, - "export" : { - "_security" : { - "group" : "group_true" + "export": { + "_security": { + "group": "group_true" }, - "allowedTypes" : [ + "allowedTypes": [ "json" ], - "allowRaw" : true + "allowRaw": true }, - "customized_fields" : [ + "customized_fields": [ "testmodel_testmodeljoin_id" ], - "availableFields" : [ + "availableFields": [ "testmodel_text" ], - "visibleFields" : [ + "visibleFields": [ "testmodel_text", "testmodel_testmodeljoin_id" ], "visibleFilters": { - "testmodel_text" : { - "wildcard" : true, - "config" : { - "field_config" : { - "field_title" : "field_config_example_title" + "testmodel_text": { + "wildcard": true, + "config": { + "field_config": { + "field_title": "field_config_example_title" } } }, - "testmodeljoin.testmodeljoin_id" : { - "wildcard" : false + "testmodeljoin.testmodeljoin_id": { + "wildcard": false } - }, - "children" : [ + }, + "children": [ "testmodel_testmodeljoin" ], - "children_config" : { - "testmodel_testmodeljoin" : { - "crud" : "crudtest_testmodeljoin_crudlistconfig", - "form" : "testmodeljoin" + "children_config": { + "testmodel_testmodeljoin": { + "crud": "crudtest_testmodeljoin_crudlistconfig", + "form": "testmodeljoin" } }, - "disabled" : [ + "disabled": [ "testmodel_unique_single", "testmodel_unique_multi1", "testmodel_unique_multi2" ], - "order" : [ + "order": [ { - "field" : "testmodel_id", - "direction" : "ASC" + "field": "testmodel_id", + "direction": "ASC" } ], - "action" : { - "crud_edit" : "example" + "action": { + "crud_edit": "example" } } diff --git a/tests/crud/config/crud/crudtest_testmodel_field_is_array.json b/tests/crud/config/crud/crudtest_testmodel_field_is_array.json index c15d33c..2b47424 100644 --- a/tests/crud/config/crud/crudtest_testmodel_field_is_array.json +++ b/tests/crud/config/crud/crudtest_testmodel_field_is_array.json @@ -3,15 +3,18 @@ "pagination": { "limit": 5 }, - "visibleFields" : [ - [ "testmodel_text", "example" ], + "visibleFields": [ + [ + "testmodel_text", + "example" + ], "testmodel_testmodeljoin_id" ], - "disabled" : [ + "disabled": [ "testmodel_unique_single", "testmodel_unique_multi1", "testmodel_unique_multi2" ], - "order" : { + "order": { } } diff --git a/tests/crud/config/crud/crudtest_testmodel_field_not_found.json b/tests/crud/config/crud/crudtest_testmodel_field_not_found.json index 8ea556c..f507441 100644 --- a/tests/crud/config/crud/crudtest_testmodel_field_not_found.json +++ b/tests/crud/config/crud/crudtest_testmodel_field_not_found.json @@ -3,17 +3,17 @@ "pagination": { "limit": 5 }, - "field" : [ + "field": [ "wrong_field" ], - "visibleFields" : [ + "visibleFields": [ "testmodel_text" ], - "disabled" : [ + "disabled": [ "testmodel_unique_single", "testmodel_unique_multi1", "testmodel_unique_multi2" ], - "order" : { + "order": { } } diff --git a/tests/crud/config/crud/crudtest_testmodel_filter.json b/tests/crud/config/crud/crudtest_testmodel_filter.json index 663b900..5ff279d 100644 --- a/tests/crud/config/crud/crudtest_testmodel_filter.json +++ b/tests/crud/config/crud/crudtest_testmodel_filter.json @@ -3,45 +3,45 @@ "pagination": { "limit": 5 }, - "displayFieldSelection" : true, - "customized_fields" : [ + "displayFieldSelection": true, + "customized_fields": [ "testmodel_testmodeljoin_id" ], - "visibleFields" : [ + "visibleFields": [ "testmodel_text", "testmodel_testmodeljoin_id" ], "visibleFilters": { - "_search" : { - "wildcard" : false, - "fields" : [ + "_search": { + "wildcard": false, + "fields": [ "testmodel_text" ] }, - "testmodel_boolean" : { - "wildcard" : false + "testmodel_boolean": { + "wildcard": false }, - "testmodel_created" : { - "wildcard" : false + "testmodel_created": { + "wildcard": false }, - "testmodel_text" : { - "wildcard" : false + "testmodel_text": { + "wildcard": false }, - "testmodel_number_natural" : { - "wildcard" : false + "testmodel_number_natural": { + "wildcard": false } - }, - "children" : [ + }, + "children": [ "testmodel_testmodeljoin" ], - "disabled" : [ + "disabled": [ "testmodel_unique_single", "testmodel_unique_multi1", "testmodel_unique_multi2" ], - "order" : { + "order": { }, - "action" : { - "crud_edit" : "example" + "action": { + "crud_edit": "example" } } diff --git a/tests/crud/config/crud/crudtest_testmodel_seek.json b/tests/crud/config/crud/crudtest_testmodel_seek.json index de10809..d984141 100644 --- a/tests/crud/config/crud/crudtest_testmodel_seek.json +++ b/tests/crud/config/crud/crudtest_testmodel_seek.json @@ -1,21 +1,21 @@ { - "seek" : true, - "pagination" : { + "seek": true, + "pagination": { "limit": 2 }, - "visibleFields" : [ + "visibleFields": [ "testmodel_text", "testmodel_flag" ], - "disabled" : [ + "disabled": [ "testmodel_unique_single", "testmodel_unique_multi1", "testmodel_unique_multi2" ], - "order" : [ + "order": [ { - "field" : "testmodel_id", - "direction" : "ASC" + "field": "testmodel_id", + "direction": "ASC" } ] } diff --git a/tests/crud/config/crud/crudtest_testmodel_wrong_children.json b/tests/crud/config/crud/crudtest_testmodel_wrong_children.json index c567bad..ba44139 100644 --- a/tests/crud/config/crud/crudtest_testmodel_wrong_children.json +++ b/tests/crud/config/crud/crudtest_testmodel_wrong_children.json @@ -3,36 +3,36 @@ "pagination": { "limit": 5 }, - "displayFieldSelection" : true, - "customized_fields" : [ + "displayFieldSelection": true, + "customized_fields": [ "testmodel_testmodeljoin_id" ], - "visibleFields" : [ + "visibleFields": [ "testmodel_text", "testmodel_testmodeljoin_id" ], "visibleFilters": { - "testmodel_text" : { - "wildcard" : true + "testmodel_text": { + "wildcard": true } - }, - "children" : [ + }, + "children": [ "wrong_children" ], - "children_config" : { - "wrong_children" : { - "crud" : "crudtest_testmodeljoin", - "form" : "testmodeljoin" + "children_config": { + "wrong_children": { + "crud": "crudtest_testmodeljoin", + "form": "testmodeljoin" } }, - "disabled" : [ + "disabled": [ "testmodel_unique_single", "testmodel_unique_multi1", "testmodel_unique_multi2" ], - "order" : { + "order": { }, - "action" : { - "crud_edit" : "example" + "action": { + "crud_edit": "example" } } diff --git a/tests/crud/config/crud/crudtest_testmodelcollection.json b/tests/crud/config/crud/crudtest_testmodelcollection.json index 95ee987..5aa9205 100644 --- a/tests/crud/config/crud/crudtest_testmodelcollection.json +++ b/tests/crud/config/crud/crudtest_testmodelcollection.json @@ -1,6 +1,6 @@ { - "displayFieldSelection" : true, - "visibleFields" : [ + "displayFieldSelection": true, + "visibleFields": [ "testmodelcollection_text" ] } diff --git a/tests/crud/config/crud/crudtest_testmodelcollectionforeign.json b/tests/crud/config/crud/crudtest_testmodelcollectionforeign.json index 95ee987..5aa9205 100644 --- a/tests/crud/config/crud/crudtest_testmodelcollectionforeign.json +++ b/tests/crud/config/crud/crudtest_testmodelcollectionforeign.json @@ -1,6 +1,6 @@ { - "displayFieldSelection" : true, - "visibleFields" : [ + "displayFieldSelection": true, + "visibleFields": [ "testmodelcollection_text" ] } diff --git a/tests/crud/config/crud/crudtest_testmodelforcejoin.json b/tests/crud/config/crud/crudtest_testmodelforcejoin.json index c553fcb..aa7506e 100644 --- a/tests/crud/config/crud/crudtest_testmodelforcejoin.json +++ b/tests/crud/config/crud/crudtest_testmodelforcejoin.json @@ -3,36 +3,36 @@ "pagination": { "limit": 5 }, - "displayFieldSelection" : true, - "customized_fields" : [ + "displayFieldSelection": true, + "customized_fields": [ "testmodelforcejoin_testmodeljoin_id" ], - "visibleFields" : [ + "visibleFields": [ "testmodelforcejoin_text", "testmodelforcejoin_testmodeljoin_id" ], "visibleFilters": { - "testmodelforcejoin_text" : { - "wildcard" : true + "testmodelforcejoin_text": { + "wildcard": true } - }, - "children" : [ + }, + "children": [ "testmodelforcejoin_testmodeljoin" ], - "children_config" : { - "testmodelforcejoin_testmodeljoin" : { - "crud" : "crudtest_testmodeljoin", - "form" : "testmodeljoin" + "children_config": { + "testmodelforcejoin_testmodeljoin": { + "crud": "crudtest_testmodeljoin", + "form": "testmodeljoin" } }, - "disabled" : [ + "disabled": [ "testmodelforcejoin_unique_single", "testmodelforcejoin_unique_multi1", "testmodelforcejoin_unique_multi2" ], - "order" : { + "order": { }, - "action" : { - "crud_edit" : "example" + "action": { + "crud_edit": "example" } } diff --git a/tests/crud/config/crud/crudtest_testmodeljoin.json b/tests/crud/config/crud/crudtest_testmodeljoin.json index c5a4d1b..2b0617b 100644 --- a/tests/crud/config/crud/crudtest_testmodeljoin.json +++ b/tests/crud/config/crud/crudtest_testmodeljoin.json @@ -1,8 +1,8 @@ { - "displayFieldSelection" : true, - "visibleFields" : [ + "displayFieldSelection": true, + "visibleFields": [ "testmodeljoin_text" ], - "order" : [ + "order": [ ] } diff --git a/tests/crud/config/crud/crudtest_testmodeljoin_crudlistconfig.json b/tests/crud/config/crud/crudtest_testmodeljoin_crudlistconfig.json index cc22d53..69419d8 100644 --- a/tests/crud/config/crud/crudtest_testmodeljoin_crudlistconfig.json +++ b/tests/crud/config/crud/crudtest_testmodeljoin_crudlistconfig.json @@ -1,5 +1,5 @@ { - "visibleFields" : [ + "visibleFields": [ "testmodeljoin_text" ] } diff --git a/tests/crud/config/crud/crudtest_testmodelwrongflag.json b/tests/crud/config/crud/crudtest_testmodelwrongflag.json index c3a5ee6..1863361 100644 --- a/tests/crud/config/crud/crudtest_testmodelwrongflag.json +++ b/tests/crud/config/crud/crudtest_testmodelwrongflag.json @@ -3,10 +3,10 @@ "pagination": { "limit": 5 }, - "visibleFields" : [ + "visibleFields": [ "testmodel_text", "testmodel_flag" ], - "order" : { + "order": { } } diff --git a/tests/crud/config/crud/form_testmodel.json b/tests/crud/config/crud/form_testmodel.json index 34e3af4..df44c13 100644 --- a/tests/crud/config/crud/form_testmodel.json +++ b/tests/crud/config/crud/form_testmodel.json @@ -1,20 +1,20 @@ { - "tag" : "example", - "children_config" : { - "testmodel_testmodeljoin" : { - "crud" : "crudtest_testmodeljoin", - "form" : "testmodeljoin" + "tag": "example", + "children_config": { + "testmodel_testmodeljoin": { + "crud": "crudtest_testmodeljoin", + "form": "testmodeljoin" } }, - "field" : [ + "field": [ "testmodel_flag", "testmodel_text", "testmodel_number_natural" ], - "required" : [ + "required": [ ], - "readonly" : [ + "readonly": [ ] } diff --git a/tests/crud/config/crud/form_testmodel_fieldsets.json b/tests/crud/config/crud/form_testmodel_fieldsets.json index 294e0be..a227e9e 100644 --- a/tests/crud/config/crud/form_testmodel_fieldsets.json +++ b/tests/crud/config/crud/form_testmodel_fieldsets.json @@ -1,22 +1,22 @@ { - "fieldset" : { - "fieldset1" : { - "field" : [ + "fieldset": { + "fieldset1": { + "field": [ "testmodel_id", "testmodel_text" ] }, - "fieldset2" : { - "field" : [ + "fieldset2": { + "field": [ "testmodel_flag", "testmodel_number_natural" ] } }, - "required" : [ + "required": [ ], - "readonly" : [ + "readonly": [ ] } diff --git a/tests/crud/config/crud/form_testmodeljoin.json b/tests/crud/config/crud/form_testmodeljoin.json index 7613e86..aad12f8 100644 --- a/tests/crud/config/crud/form_testmodeljoin.json +++ b/tests/crud/config/crud/form_testmodeljoin.json @@ -1,14 +1,14 @@ { - "tag" : "example", - "children_config" : { + "tag": "example", + "children_config": { }, - "field" : [ + "field": [ "testmodeljoin_text" ], - "required" : [ + "required": [ "testmodeljoin_text" ], - "readonly" : [ + "readonly": [ "testmodeljoin_id" ] } diff --git a/tests/crud/crudCreateTest.php b/tests/crud/crudCreateTest.php index aa13214..8c115bc 100644 --- a/tests/crud/crudCreateTest.php +++ b/tests/crud/crudCreateTest.php @@ -1,431 +1,519 @@ getModel('testmodel') - ->addFilter('testmodel_id', 0, '>') - ->delete(); - - $this->getModel('testmodeljoin') - ->addFilter('testmodeljoin_id', 0, '>') - ->delete(); - - $this->getModel('testmodelforcejoin') - ->addFilter('testmodelforcejoin_id', 0, '>') - ->delete(); - - $this->getModel('testmodelcollection') - ->addFilter('testmodelcollection_id', 0, '>') - ->delete(); - } - - /** - * @inheritDoc - */ - protected function setUp(): void - { - overrideableApp::resetRequest(); - overrideableApp::resetResponse(); - $app = static::createApp(); - - // Additional overrides to get a more complete app lifecycle - // and allow static global app::getModel() to work correctly - $app->__setApp('crudtest'); - $app->__setVendor('codename'); - $app->__setNamespace('\\codename\\core\\ui\\tests\\crud'); - $app->__setHomedir(__DIR__); - - $app->getAppstack(); - - // avoid re-init - if(static::$initialized) { - return; +use codename\core\ui\crud; +use codename\core\ui\field; +use codename\core\ui\form; +use codename\core\ui\tests\crud\model\testmodel; +use codename\core\ui\tests\crud\model\testmodelcollection; +use codename\core\ui\tests\crud\model\testmodelforcejoin; +use codename\core\ui\tests\crud\model\testmodeljoin; +use DateMalformedStringException; +use ErrorException; +use Exception; +use ReflectionException; +use Throwable; + +class crudCreateTest extends base +{ + /** + * [protected description] + * @var bool + */ + protected static bool $initialized = false; + + /** + * {@inheritDoc} + */ + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + static::$initialized = false; } - static::$initialized = true; - - static::setEnvironmentConfig([ - 'test' => [ - 'database' => [ - // NOTE: by default, we do these tests using - // pure in-memory sqlite. - 'default' => [ - 'driver' => 'sqlite', - // 'database_file' => 'testmodel.sqlite', - 'database_file' => ':memory:', - ], - ], - 'cache' => [ - 'default' => [ - 'driver' => 'memory' - ] - ], - 'filesystem' =>[ - 'local' => [ - 'driver' => 'local', - ] - ], - 'translate' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - 'templateengine' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - 'log' => [ - 'default' => [ - 'driver' => 'system', - 'data' => [ - 'name' => 'dummy' - ] - ] - ], - ] - ]); - - static::createModel( - 'crudtest', 'testmodel', - \codename\core\ui\tests\crud\model\testmodel::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodel([]); - } - ); - - static::createModel( - 'crudtest', 'testmodeljoin', - \codename\core\ui\tests\crud\model\testmodeljoin::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodeljoin([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelforcejoin', - \codename\core\ui\tests\crud\model\testmodelforcejoin::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelforcejoin([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelcollection', - \codename\core\ui\tests\crud\model\testmodelcollection::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelcollection([]); - } - ); - - static::architect('crudtest', 'codename', 'test'); - } - - /** - * [testCrudCreateForm description] - */ - public function testCrudCreateForm(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - - $crudInstance->eventCrudFormInit->addEventHandler(new \codename\core\eventHandler(function(\codename\core\ui\form $form) { - $form->fields = array_filter($form->fields, function(\codename\core\ui\field $item) { - return $item->getConfig()->get('field_type') !== 'submit'; // only allow non-submits - }); - return $form; - })); - - $crudInstance->create(); - - // renderer? - $this->assertEquals('frontend/form/compact/form', \codename\core\app::getResponse()->getData('form')); - - $form = $crudInstance->getForm(); - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - $this->assertEquals('hidden', $form->getField('testmodel_id')->getProperty('field_type')); - $this->assertEquals('input', $form->getField('testmodel_text')->getProperty('field_type')); - - $fields = $form->getFields(); - $this->assertCount(8, $fields); - } - - /** - * [testCrudCreateFormSendSuccess description] - */ - public function testCrudCreateFormSendSuccess(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->create(); - - // renderer? - $this->assertEquals('frontend/form/compact/form', \codename\core\app::getResponse()->getData('form')); - - $form = $crudInstance->getForm(); - - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - $formSentField = null; - foreach($form->getFields() as $field) { - // detect form sent field - if(strpos($field->getProperty('field_name'), 'formSent') === 0) { - $formSentField = $field; - } + /** + * [testCrudCreateForm description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudCreateForm(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + $crudInstance->eventCrudFormInit->addEventHandler( + new eventHandler(function (form $form) { + $form->fields = array_filter($form->fields, function (field $item) { + return $item->getConfig()->get('field_type') !== 'submit'; // only allow non-submits + }); + return $form; + }) + ); + + $crudInstance->create(); + + // renderer? + static::assertEquals('frontend/form/compact/form', app::getResponse()->getData('form')); + + $form = $crudInstance->getForm(); + static::assertInstanceOf(form::class, $form); + static::assertEquals('hidden', $form->getField('testmodel_id')->getProperty('field_type')); + static::assertEquals('input', $form->getField('testmodel_text')->getProperty('field_type')); + + $fields = $form->getFields(); + static::assertCount(8, $fields); } - // emulate a request 'submitting' the form - \codename\core\app::getRequest()->setData($formSentField->getProperty('field_name'), 1); - \codename\core\app::getRequest()->setData('testmodel_text', 'abc'); - - // create a new crud instance to simulate creation - // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error - $model = $this->getModel('testmodel'); - $saveCrudInstance = new \codename\core\ui\crud($model); - $saveCrudInstance->create(); - - $res = $model->search()->getResult(); - $this->assertCount(1, $res); - } - - /** - * [testCrudCreateFormSendSuccessWithEventCrudBeforeSave description] - */ - public function testCrudCreateFormSendSuccessWithEventCrudBeforeSave(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->create(); - - // renderer? - $this->assertEquals('frontend/form/compact/form', \codename\core\app::getResponse()->getData('form')); - - $form = $crudInstance->getForm(); - - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - $formSentField = null; - foreach($form->getFields() as $field) { - // detect form sent field - if(strpos($field->getProperty('field_name'), 'formSent') === 0) { - $formSentField = $field; - } + /** + * [testCrudCreateFormSendSuccess description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudCreateFormSendSuccess(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->create(); + + // renderer? + static::assertEquals('frontend/form/compact/form', app::getResponse()->getData('form')); + + $form = $crudInstance->getForm(); + + static::assertInstanceOf(form::class, $form); + + $formSentField = null; + foreach ($form->getFields() as $field) { + // detect form sent field + if (str_starts_with($field->getConfig()->get('field_name'), 'formSent')) { + $formSentField = $field; + } + } + + // emulate a request 'submitting' the form + app::getRequest()->setData($formSentField->getConfig()->get('field_name'), 1); + app::getRequest()->setData('testmodel_text', 'abc'); + + // create a new crud instance to simulate creation + // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error + $model = $this->getModel('testmodel'); + $saveCrudInstance = new crud($model); + $saveCrudInstance->create(); + + $res = $model->search()->getResult(); + static::assertCount(1, $res); } - // emulate a request 'submitting' the form - \codename\core\app::getRequest()->setData($formSentField->getProperty('field_name'), 1); - \codename\core\app::getRequest()->setData('testmodel_text', 'abc'); - - // create a new crud instance to simulate creation - // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error - $model = $this->getModel('testmodel'); - $saveCrudInstance = new \codename\core\ui\crud($model); - - $saveCrudInstance->eventCrudBeforeSave->addEventHandler(new \codename\core\eventHandler(function(array $data) {})); - - $saveCrudInstance->create(); - - $res = $model->search()->getResult(); - $this->assertCount(1, $res); - - $this->assertEquals('abc', $res[0]['testmodel_text']); - } - - /** - * [testCrudCreateFormInvalid description] - */ - public function testCrudCreateFormInvalid(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->create(); - - // renderer? - $this->assertEquals('frontend/form/compact/form', \codename\core\app::getResponse()->getData('form')); - - $form = $crudInstance->getForm(); - - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - $formSentField = null; - foreach($form->getFields() as $field) { - // detect form sent field - if(strpos($field->getProperty('field_name'), 'formSent') === 0) { - $formSentField = $field; - } + /** + * [testCrudCreateFormSendSuccessWithEventCrudBeforeSave description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudCreateFormSendSuccessWithEventCrudBeforeSave(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->create(); + + // renderer? + static::assertEquals('frontend/form/compact/form', app::getResponse()->getData('form')); + + $form = $crudInstance->getForm(); + + static::assertInstanceOf(form::class, $form); + + $formSentField = null; + foreach ($form->getFields() as $field) { + // detect form sent field + if (str_starts_with($field->getConfig()->get('field_name'), 'formSent')) { + $formSentField = $field; + } + } + + // emulate a request 'submitting' the form + app::getRequest()->setData($formSentField->getConfig()->get('field_name'), 1); + app::getRequest()->setData('testmodel_text', 'abc'); + + // create a new crud instance to simulate creation + // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error + $model = $this->getModel('testmodel'); + $saveCrudInstance = new crud($model); + + $saveCrudInstance->eventCrudBeforeSave->addEventHandler( + new eventHandler(function (array $data) { + }) + ); + + $saveCrudInstance->create(); + + $res = $model->search()->getResult(); + static::assertCount(1, $res); + + static::assertEquals('abc', $res[0]['testmodel_text']); } - // emulate a request 'submitting' the form - \codename\core\app::getRequest()->setData($formSentField->getProperty('field_name'), 1); - \codename\core\app::getRequest()->setData('testmodel_text', 'abc'); - - // create a new crud instance to simulate creation - // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error - $model = $this->getModel('testmodel'); - $saveCrudInstance = new \codename\core\ui\crud($model); - - $saveCrudInstance->onCreateFormfield = function(array &$fielddata) { - if($fielddata['field_name'] === 'testmodel_text') { - $fielddata['field_datatype'] = 'number_natural'; - } - }; - - $this->assertEmpty($saveCrudInstance->create()); - - $this->assertEquals('validation_error', \codename\core\app::getResponse()->getData('view')); - $errors = \codename\core\app::getResponse()->getData('errors'); - - $this->assertCount(1, $errors); - $this->assertEquals([ - [ - '__IDENTIFIER' => 'testmodel_text', - '__CODE' => 'VALIDATION.FIELD_INVALID', - '__TYPE' => 'VALIDATION', - '__DETAILS' => [ + /** + * [testCrudCreateFormInvalid description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudCreateFormInvalid(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->create(); + + // renderer? + static::assertEquals('frontend/form/compact/form', app::getResponse()->getData('form')); + + $form = $crudInstance->getForm(); + + static::assertInstanceOf(form::class, $form); + + $formSentField = null; + foreach ($form->getFields() as $field) { + // detect form sent field + if (str_starts_with($field->getConfig()->get('field_name'), 'formSent')) { + $formSentField = $field; + } + } + + // emulate a request 'submitting' the form + app::getRequest()->setData($formSentField->getConfig()->get('field_name'), 1); + app::getRequest()->setData('testmodel_text', 'abc'); + + // create a new crud instance to simulate creation + // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error + $model = $this->getModel('testmodel'); + $saveCrudInstance = new crud($model); + + $saveCrudInstance->onCreateFormfield = function (array &$fielddata) { + if ($fielddata['field_name'] === 'testmodel_text') { + $fielddata['field_datatype'] = 'number_natural'; + } + }; + + try { + $saveCrudInstance->create(); + } catch (Exception) { + static::fail(); + } + + static::assertEquals('validation_error', app::getResponse()->getData('view')); + $errors = app::getResponse()->getData('errors'); + + static::assertCount(1, $errors); + static::assertEquals([ [ - '__IDENTIFIER' => 'VALUE', - '__CODE' => 'VALIDATION.VALUE_NOT_A_NUMBER', - '__TYPE' => 'VALIDATION', - '__DETAILS' => 'abc', + '__IDENTIFIER' => 'testmodel_text', + '__CODE' => 'VALIDATION.FIELD_INVALID', + '__TYPE' => 'VALIDATION', + '__DETAILS' => [ + [ + '__IDENTIFIER' => 'VALUE', + '__CODE' => 'VALIDATION.VALUE_NOT_A_NUMBER', + '__TYPE' => 'VALIDATION', + '__DETAILS' => 'abc', + ], + ], ], - ], - ] - ], $errors); - } - - /** - * [testCrudCreateModelInvalid description] - */ - public function testCrudCreateModelInvalid(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->create(); - - // renderer? - $this->assertEquals('frontend/form/compact/form', \codename\core\app::getResponse()->getData('form')); - - $form = $crudInstance->getForm(); - - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - $formSentField = null; - foreach($form->getFields() as $field) { - // detect form sent field - if(strpos($field->getProperty('field_name'), 'formSent') === 0) { - $formSentField = $field; - } + ], $errors); } - // emulate a request 'submitting' the form - \codename\core\app::getRequest()->setData($formSentField->getProperty('field_name'), 1); - \codename\core\app::getRequest()->setData('testmodel_text', 'abc'); - - // create a new crud instance to simulate creation - // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error - $model = $this->getModel('testmodel'); - $saveCrudInstance = new \codename\core\ui\crud($model); - - $saveCrudInstance->eventCrudBeforeValidation->addEventHandler(new \codename\core\eventHandler(function(array $data) { - $data['testmodel_testmodeljoin_id'] = 'abc'; - return $data; - })); - - $this->assertEmpty($saveCrudInstance->create()); - - $this->assertEquals('save_error', \codename\core\app::getResponse()->getData('view')); - $errors = \codename\core\app::getResponse()->getData('errors'); - - $this->assertCount(1, $errors); - $this->assertEquals([ - [ - '__IDENTIFIER' => 'testmodel_testmodeljoin_id', - '__CODE' => 'VALIDATION.FIELD_INVALID', - '__TYPE' => 'VALIDATION', - '__DETAILS' => [ + /** + * [testCrudCreateModelInvalid description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudCreateModelInvalid(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->create(); + + // renderer? + static::assertEquals('frontend/form/compact/form', app::getResponse()->getData('form')); + + $form = $crudInstance->getForm(); + + static::assertInstanceOf(form::class, $form); + + $formSentField = null; + foreach ($form->getFields() as $field) { + // detect form sent field + if (str_starts_with($field->getConfig()->get('field_name'), 'formSent')) { + $formSentField = $field; + } + } + + // emulate a request 'submitting' the form + app::getRequest()->setData($formSentField->getConfig()->get('field_name'), 1); + app::getRequest()->setData('testmodel_text', 'abc'); + + // create a new crud instance to simulate creation + // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error + $model = $this->getModel('testmodel'); + $saveCrudInstance = new crud($model); + + $saveCrudInstance->eventCrudBeforeValidation->addEventHandler( + new eventHandler(function (array $data) { + $data['testmodel_testmodeljoin_id'] = 'abc'; + return $data; + }) + ); + + try { + $saveCrudInstance->create(); + } catch (Exception) { + static::fail(); + } + + static::assertEquals('save_error', app::getResponse()->getData('view')); + $errors = app::getResponse()->getData('errors'); + + static::assertCount(1, $errors); + static::assertEquals([ [ - '__IDENTIFIER' => 'VALUE', - '__CODE' => 'VALIDATION.VALUE_NOT_A_NUMBER', - '__TYPE' => 'VALIDATION', - '__DETAILS' => 'abc', + '__IDENTIFIER' => 'testmodel_testmodeljoin_id', + '__CODE' => 'VALIDATION.FIELD_INVALID', + '__TYPE' => 'VALIDATION', + '__DETAILS' => [ + [ + '__IDENTIFIER' => 'VALUE', + '__CODE' => 'VALIDATION.VALUE_NOT_A_NUMBER', + '__TYPE' => 'VALIDATION', + '__DETAILS' => 'abc', + ], + ], ], - ], - ] - ], $errors); - } - - /** - * [testCrudCreateValidationError description] - */ - public function testCrudCreateValidationError(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->create(); - - // renderer? - $this->assertEquals('frontend/form/compact/form', \codename\core\app::getResponse()->getData('form')); - - $form = $crudInstance->getForm(); - - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - $formSentField = null; - foreach($form->getFields() as $field) { - // detect form sent field - if(strpos($field->getProperty('field_name'), 'formSent') === 0) { - $formSentField = $field; - } + ], $errors); } - // emulate a request 'submitting' the form - \codename\core\app::getRequest()->setData($formSentField->getProperty('field_name'), 1); - \codename\core\app::getRequest()->setData('testmodel_text', 'abc'); - - // create a new crud instance to simulate creation - // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error - $model = $this->getModel('testmodel'); - $saveCrudInstance = new \codename\core\ui\crud($model); - - $saveCrudInstance->eventCrudValidation->addEventHandler(new \codename\core\eventHandler(function($data) { - $errors = new \codename\core\errorstack('VALIDATION'); - $errors->addError('EXAMPLE', 'EXAMPLE', 'EXAMPLE'); - - return $errors->getErrors(); - })); - - $this->assertEmpty($saveCrudInstance->create()); - - $this->assertEquals('save_error', \codename\core\app::getResponse()->getData('view')); - $errors = \codename\core\app::getResponse()->getData('errors'); + /** + * [testCrudCreateValidationError description] + * @throws ReflectionException + * @throws DateMalformedStringException + * @throws \codename\core\exception + */ + public function testCrudCreateValidationError(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->create(); + + // renderer? + static::assertEquals('frontend/form/compact/form', app::getResponse()->getData('form')); + + $form = $crudInstance->getForm(); + + static::assertInstanceOf(form::class, $form); + + $formSentField = null; + foreach ($form->getFields() as $field) { + // detect form sent field + if (str_starts_with($field->getConfig()->get('field_name'), 'formSent')) { + $formSentField = $field; + } + } + + // emulate a request 'submitting' the form + app::getRequest()->setData($formSentField->getConfig()->get('field_name'), 1); + app::getRequest()->setData('testmodel_text', 'abc'); + + // create a new crud instance to simulate creation + // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error + $model = $this->getModel('testmodel'); + $saveCrudInstance = new crud($model); + + $saveCrudInstance->eventCrudValidation->addEventHandler( + new eventHandler(function ($data) { + $errors = new errorstack('VALIDATION'); + $errors->addError('EXAMPLE', 'EXAMPLE', 'EXAMPLE'); + + return $errors->getErrors(); + }) + ); + + try { + $saveCrudInstance->create(); + } catch (Exception) { + static::fail(); + } + + static::assertEquals('save_error', app::getResponse()->getData('view')); + $errors = app::getResponse()->getData('errors'); + + static::assertCount(1, $errors); + static::assertEquals([ + [ + '__IDENTIFIER' => 'EXAMPLE', + '__CODE' => 'VALIDATION.EXAMPLE', + '__TYPE' => 'VALIDATION', + '__DETAILS' => 'EXAMPLE', + ], + ], $errors); + } - $this->assertCount(1, $errors); - $this->assertEquals([ - [ - '__IDENTIFIER' => 'EXAMPLE', - '__CODE' => 'VALIDATION.EXAMPLE', - '__TYPE' => 'VALIDATION', - '__DETAILS' => 'EXAMPLE', - ] - ], $errors); - } + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws \codename\core\exception + */ + protected function tearDown(): void + { + $this->getModel('testmodel') + ->addFilter('testmodel_id', 0, '>') + ->delete(); + + $this->getModel('testmodeljoin') + ->addFilter('testmodeljoin_id', 0, '>') + ->delete(); + + $this->getModel('testmodelforcejoin') + ->addFilter('testmodelforcejoin_id', 0, '>') + ->delete(); + + $this->getModel('testmodelcollection') + ->addFilter('testmodelcollection_id', 0, '>') + ->delete(); + } + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws ErrorException + * @throws Throwable + * @throws compileErrorException + * @throws coreErrorException + * @throws coreWarningException + * @throws parseException + * @throws recoverableErrorException + * @throws strictException + * @throws userErrorException + * @throws userWarningException + * @throws warningException + * @throws \codename\core\exception + */ + protected function setUp(): void + { + overrideableApp::resetRequest(); + overrideableApp::resetResponse(); + $app = static::createApp(); + + // Additional overrides to get a more complete app lifecycle + // and allow static global app::getModel() to work correctly + $app::__setApp('crudtest'); + $app::__setVendor('codename'); + $app::__setNamespace('\\codename\\core\\ui\\tests\\crud'); + $app::__setHomedir(__DIR__); + + $app::getAppstack(); + + // avoid re-init + if (static::$initialized) { + return; + } + + static::$initialized = true; + + static::setEnvironmentConfig([ + 'test' => [ + 'database' => [ + // NOTE: by default, we do these tests using + // pure in-memory sqlite. + 'default' => [ + 'driver' => 'sqlite', + // 'database_file' => 'testmodel.sqlite', + 'database_file' => ':memory:', + ], + ], + 'cache' => [ + 'default' => [ + 'driver' => 'memory', + ], + ], + 'filesystem' => [ + 'local' => [ + 'driver' => 'local', + ], + ], + 'translate' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + 'templateengine' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + 'log' => [ + 'default' => [ + 'driver' => 'system', + 'data' => [ + 'name' => 'dummy', + ], + ], + ], + ], + ]); + + static::createModel( + 'crudtest', + 'testmodel', + testmodel::$staticConfig, + function ($schema, $model, $config) { + return new testmodel([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodeljoin', + testmodeljoin::$staticConfig, + function ($schema, $model, $config) { + return new testmodeljoin([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelforcejoin', + testmodelforcejoin::$staticConfig, + function ($schema, $model, $config) { + return new testmodelforcejoin([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelcollection', + testmodelcollection::$staticConfig, + function ($schema, $model, $config) { + return new testmodelcollection([]); + } + ); + + static::architect('crudtest', 'codename', 'test'); + } } diff --git a/tests/crud/crudEditTest.php b/tests/crud/crudEditTest.php index c28aeb9..ab1f166 100644 --- a/tests/crud/crudEditTest.php +++ b/tests/crud/crudEditTest.php @@ -1,488 +1,576 @@ getModel('testmodel') - ->addFilter('testmodel_id', 0, '>') - ->delete(); - - $this->getModel('testmodeljoin') - ->addFilter('testmodeljoin_id', 0, '>') - ->delete(); - - $this->getModel('testmodelforcejoin') - ->addFilter('testmodelforcejoin_id', 0, '>') - ->delete(); - - $this->getModel('testmodelcollection') - ->addFilter('testmodelcollection_id', 0, '>') - ->delete(); - } - - /** - * @inheritDoc - */ - protected function setUp(): void - { - overrideableApp::resetRequest(); - overrideableApp::resetResponse(); - $app = static::createApp(); - - // Additional overrides to get a more complete app lifecycle - // and allow static global app::getModel() to work correctly - $app->__setApp('crudtest'); - $app->__setVendor('codename'); - $app->__setNamespace('\\codename\\core\\ui\\tests\\crud'); - $app->__setHomedir(__DIR__); - - $app->getAppstack(); - - // avoid re-init - if(static::$initialized) { - return; +use codename\core\ui\crud; +use codename\core\ui\field; +use codename\core\ui\form; +use codename\core\ui\tests\crud\model\testmodel; +use codename\core\ui\tests\crud\model\testmodelcollection; +use codename\core\ui\tests\crud\model\testmodelforcejoin; +use codename\core\ui\tests\crud\model\testmodeljoin; +use DateMalformedStringException; +use ErrorException; +use Exception; +use ReflectionException; +use Throwable; + +class crudEditTest extends base +{ + /** + * [protected description] + * @var bool + */ + protected static bool $initialized = false; + + /** + * {@inheritDoc} + */ + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + static::$initialized = false; } - static::$initialized = true; - - static::setEnvironmentConfig([ - 'test' => [ - 'database' => [ - // NOTE: by default, we do these tests using - // pure in-memory sqlite. - 'default' => [ - 'driver' => 'sqlite', - // 'database_file' => 'testmodel.sqlite', - 'database_file' => ':memory:', - ], - ], - 'cache' => [ - 'default' => [ - 'driver' => 'memory' - ] - ], - 'filesystem' =>[ - 'local' => [ - 'driver' => 'local', - ] - ], - 'translate' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - 'templateengine' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - 'log' => [ - 'default' => [ - 'driver' => 'system', - 'data' => [ - 'name' => 'dummy' - ] - ] - ], - ] - ]); - - static::createModel( - 'crudtest', 'testmodel', - \codename\core\ui\tests\crud\model\testmodel::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodel([]); - } - ); - - static::createModel( - 'crudtest', 'testmodeljoin', - \codename\core\ui\tests\crud\model\testmodeljoin::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodeljoin([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelforcejoin', - \codename\core\ui\tests\crud\model\testmodelforcejoin::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelforcejoin([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelcollection', - \codename\core\ui\tests\crud\model\testmodelcollection::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelcollection([]); - } - ); - - static::architect('crudtest', 'codename', 'test'); - } - - /** - * [testCrudEditForm description] - */ - public function testCrudEditForm(): void { - $model = $this->getModel('testmodel'); - - // insert an entry and pass on PKEY for further 'editing' - $model->save([ - 'testmodel_text' => 'XYZ' - ]); - $id = $model->lastInsertId(); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - - $crudInstance->eventCrudFormInit->addEventHandler(new \codename\core\eventHandler(function(\codename\core\ui\form $form) { - $form->fields = array_filter($form->fields, function(\codename\core\ui\field $item) { - return $item->getConfig()->get('field_type') !== 'submit'; // only allow non-submits - }); - return $form; - })); - - $crudInstance->edit($id); - - // renderer? - $this->assertEquals('frontend/form/compact/form', \codename\core\app::getResponse()->getData('form')); - - $form = $crudInstance->getForm(); - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - $this->assertEquals('hidden', $form->getField('testmodel_id')->getProperty('field_type')); - $this->assertEquals('input', $form->getField('testmodel_text')->getProperty('field_type')); - - $fields = $form->getFields(); - $this->assertCount(8, $fields); - - // obsolete? - $this->assertEquals('example', \codename\core\app::getResponse()->getData('editActions')); - } - - /** - * [testCrudEditFormSendSuccess description] - */ - public function testCrudEditFormSendSuccess(): void { - $model = $this->getModel('testmodel'); - - // insert an entry and pass on PKEY for further 'editing' - $model->save([ - 'testmodel_text' => 'XYZ' - ]); - $id = $model->lastInsertId(); - - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->edit($id); - - // renderer? - $this->assertEquals('frontend/form/compact/form', \codename\core\app::getResponse()->getData('form')); - - $form = $crudInstance->getForm(); - - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - // make sure we're loading the right entry - // and the value is prefilled - $this->assertEquals('XYZ', $form->getField('testmodel_text')->getConfig()->get('field_value')); - - $formSentField = null; - foreach($form->getFields() as $field) { - // detect form sent field - if(strpos($field->getProperty('field_name'), 'formSent') === 0) { - $formSentField = $field; - } + /** + * [testCrudEditForm description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudEditForm(): void + { + $model = $this->getModel('testmodel'); + + // insert an entry and pass on PKEY for further 'editing' + $model->save([ + 'testmodel_text' => 'XYZ', + ]); + $id = $model->lastInsertId(); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + $crudInstance->eventCrudFormInit->addEventHandler( + new eventHandler(function (form $form) { + $form->fields = array_filter($form->fields, function (field $item) { + return $item->getConfig()->get('field_type') !== 'submit'; // only allow non-submits + }); + return $form; + }) + ); + + $crudInstance->edit($id); + + // renderer? + static::assertEquals('frontend/form/compact/form', app::getResponse()->getData('form')); + + $form = $crudInstance->getForm(); + static::assertInstanceOf(form::class, $form); + static::assertEquals('hidden', $form->getField('testmodel_id')->getProperty('field_type')); + static::assertEquals('input', $form->getField('testmodel_text')->getProperty('field_type')); + + $fields = $form->getFields(); + static::assertCount(8, $fields); + + // obsolete? + static::assertEquals('example', app::getResponse()->getData('editActions')); } - // emulate a request 'submitting' the form - \codename\core\app::getRequest()->setData($formSentField->getProperty('field_name'), 1); - \codename\core\app::getRequest()->setData('testmodel_text', 'changed'); - - // create a new crud instance to simulate creation - // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error - $model = $this->getModel('testmodel'); - $saveCrudInstance = new \codename\core\ui\crud($model); - $saveCrudInstance->edit($id); - - $res = $model->search()->getResult(); - $this->assertCount(1, $res); - $this->assertEquals('changed', $res[0]['testmodel_text']); - } - - /** - * [testCrudEditFormSendSuccessWithEventCrudBeforeSave description] - */ - public function testCrudEditFormSendSuccessWithEventCrudBeforeSave(): void { - $model = $this->getModel('testmodel'); - - // insert an entry and pass on PKEY for further 'editing' - $model->save([ - 'testmodel_text' => 'XYZ' - ]); - $id = $model->lastInsertId(); - - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->edit($id); - - // renderer? - $this->assertEquals('frontend/form/compact/form', \codename\core\app::getResponse()->getData('form')); - - $form = $crudInstance->getForm(); - - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - // make sure we're loading the right entry - // and the value is prefilled - $this->assertEquals('XYZ', $form->getField('testmodel_text')->getConfig()->get('field_value')); - - $formSentField = null; - foreach($form->getFields() as $field) { - // detect form sent field - if(strpos($field->getProperty('field_name'), 'formSent') === 0) { - $formSentField = $field; - } + /** + * [testCrudEditFormSendSuccess description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudEditFormSendSuccess(): void + { + $model = $this->getModel('testmodel'); + + // insert an entry and pass on PKEY for further 'editing' + $model->save([ + 'testmodel_text' => 'XYZ', + ]); + $id = $model->lastInsertId(); + + $crudInstance = new crud($model); + $crudInstance->edit($id); + + // renderer? + static::assertEquals('frontend/form/compact/form', app::getResponse()->getData('form')); + + $form = $crudInstance->getForm(); + + static::assertInstanceOf(form::class, $form); + + // make sure we're loading the right entry + // and the value is prefilled + static::assertEquals('XYZ', $form->getField('testmodel_text')->getProperty('field_value')); + + $formSentField = null; + foreach ($form->getFields() as $field) { + // detect form sent field + if (str_starts_with($field->getConfig()->get('field_name'), 'formSent')) { + $formSentField = $field; + } + } + + // emulate a request 'submitting' the form + app::getRequest()->setData($formSentField->getConfig()->get('field_name'), 1); + app::getRequest()->setData('testmodel_text', 'changed'); + + // create a new crud instance to simulate creation + // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error + $model = $this->getModel('testmodel'); + $saveCrudInstance = new crud($model); + $saveCrudInstance->edit($id); + + $res = $model->search()->getResult(); + static::assertCount(1, $res); + static::assertEquals('changed', $res[0]['testmodel_text']); } - // emulate a request 'submitting' the form - \codename\core\app::getRequest()->setData($formSentField->getProperty('field_name'), 1); - \codename\core\app::getRequest()->setData('testmodel_text', 'changed'); - - // create a new crud instance to simulate creation - // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error - $model = $this->getModel('testmodel'); - $saveCrudInstance = new \codename\core\ui\crud($model); - - $saveCrudInstance->eventCrudBeforeSave->addEventHandler(new \codename\core\eventHandler(function(array $data) {})); - - $saveCrudInstance->edit($id); - - $res = $model->search()->getResult(); - $this->assertCount(1, $res); - $this->assertEquals('changed', $res[0]['testmodel_text']); - } - - /** - * [testCrudEditFormInvalid description] - */ - public function testCrudEditFormInvalid(): void { - $model = $this->getModel('testmodel'); - - // insert an entry and pass on PKEY for further 'editing' - $model->save([ - 'testmodel_text' => 'XYZ' - ]); - $id = $model->lastInsertId(); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->edit($id); - - // renderer? - $this->assertEquals('frontend/form/compact/form', \codename\core\app::getResponse()->getData('form')); - - $form = $crudInstance->getForm(); - - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - $formSentField = null; - foreach($form->getFields() as $field) { - // detect form sent field - if(strpos($field->getProperty('field_name'), 'formSent') === 0) { - $formSentField = $field; - } + /** + * [testCrudEditFormSendSuccessWithEventCrudBeforeSave description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudEditFormSendSuccessWithEventCrudBeforeSave(): void + { + $model = $this->getModel('testmodel'); + + // insert an entry and pass on PKEY for further 'editing' + $model->save([ + 'testmodel_text' => 'XYZ', + ]); + $id = $model->lastInsertId(); + + $crudInstance = new crud($model); + $crudInstance->edit($id); + + // renderer? + static::assertEquals('frontend/form/compact/form', app::getResponse()->getData('form')); + + $form = $crudInstance->getForm(); + + static::assertInstanceOf(form::class, $form); + + // make sure we're loading the right entry + // and the value is prefilled + static::assertEquals('XYZ', $form->getField('testmodel_text')->getProperty('field_value')); + + $formSentField = null; + foreach ($form->getFields() as $field) { + // detect form sent field + if (str_starts_with($field->getConfig()->get('field_name'), 'formSent')) { + $formSentField = $field; + } + } + + // emulate a request 'submitting' the form + app::getRequest()->setData($formSentField->getConfig()->get('field_name'), 1); + app::getRequest()->setData('testmodel_text', 'changed'); + + // create a new crud instance to simulate creation + // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error + $model = $this->getModel('testmodel'); + $saveCrudInstance = new crud($model); + + $saveCrudInstance->eventCrudBeforeSave->addEventHandler( + new eventHandler(function (array $data) { + }) + ); + + $saveCrudInstance->edit($id); + + $res = $model->search()->getResult(); + static::assertCount(1, $res); + static::assertEquals('changed', $res[0]['testmodel_text']); } - // emulate a request 'submitting' the form - \codename\core\app::getRequest()->setData($formSentField->getProperty('field_name'), 1); - \codename\core\app::getRequest()->setData('testmodel_text', 'abc'); - - // create a new crud instance to simulate creation - // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error - $model = $this->getModel('testmodel'); - $saveCrudInstance = new \codename\core\ui\crud($model); - - $saveCrudInstance->onCreateFormfield = function(array &$fielddata) { - if($fielddata['field_name'] === 'testmodel_text') { - $fielddata['field_datatype'] = 'number_natural'; - } - }; - - $this->assertEmpty($saveCrudInstance->edit($id)); - - $this->assertEquals('validation_error', \codename\core\app::getResponse()->getData('view')); - $errors = \codename\core\app::getResponse()->getData('errors'); - - $this->assertCount(1, $errors); - $this->assertEquals([ - [ - '__IDENTIFIER' => 'testmodel_text', - '__CODE' => 'VALIDATION.FIELD_INVALID', - '__TYPE' => 'VALIDATION', - '__DETAILS' => [ + /** + * [testCrudEditFormInvalid description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudEditFormInvalid(): void + { + $model = $this->getModel('testmodel'); + + // insert an entry and pass on PKEY for further 'editing' + $model->save([ + 'testmodel_text' => 'XYZ', + ]); + $id = $model->lastInsertId(); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->edit($id); + + // renderer? + static::assertEquals('frontend/form/compact/form', app::getResponse()->getData('form')); + + $form = $crudInstance->getForm(); + + static::assertInstanceOf(form::class, $form); + + $formSentField = null; + foreach ($form->getFields() as $field) { + // detect form sent field + if (str_starts_with($field->getConfig()->get('field_name'), 'formSent')) { + $formSentField = $field; + } + } + + // emulate a request 'submitting' the form + app::getRequest()->setData($formSentField->getConfig()->get('field_name'), 1); + app::getRequest()->setData('testmodel_text', 'abc'); + + // create a new crud instance to simulate creation + // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error + $model = $this->getModel('testmodel'); + $saveCrudInstance = new crud($model); + + $saveCrudInstance->onCreateFormfield = function (array &$fielddata) { + if ($fielddata['field_name'] === 'testmodel_text') { + $fielddata['field_datatype'] = 'number_natural'; + } + }; + + try { + $saveCrudInstance->edit($id); + } catch (Exception) { + static::fail(); + } + + static::assertEquals('validation_error', app::getResponse()->getData('view')); + $errors = app::getResponse()->getData('errors'); + + static::assertCount(1, $errors); + static::assertEquals([ [ - '__IDENTIFIER' => 'VALUE', - '__CODE' => 'VALIDATION.VALUE_NOT_A_NUMBER', - '__TYPE' => 'VALIDATION', - '__DETAILS' => 'abc', + '__IDENTIFIER' => 'testmodel_text', + '__CODE' => 'VALIDATION.FIELD_INVALID', + '__TYPE' => 'VALIDATION', + '__DETAILS' => [ + [ + '__IDENTIFIER' => 'VALUE', + '__CODE' => 'VALIDATION.VALUE_NOT_A_NUMBER', + '__TYPE' => 'VALIDATION', + '__DETAILS' => 'abc', + ], + ], ], - ], - ] - ], $errors); - } - - /** - * [testCrudEditModelInvalid description] - */ - public function testCrudEditModelInvalid(): void { - $model = $this->getModel('testmodel'); - - // insert an entry and pass on PKEY for further 'editing' - $model->save([ - 'testmodel_text' => 'XYZ' - ]); - $id = $model->lastInsertId(); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->edit($id); - - // renderer? - $this->assertEquals('frontend/form/compact/form', \codename\core\app::getResponse()->getData('form')); - - $form = $crudInstance->getForm(); - - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - $formSentField = null; - foreach($form->getFields() as $field) { - // detect form sent field - if(strpos($field->getProperty('field_name'), 'formSent') === 0) { - $formSentField = $field; - } + ], $errors); } - // emulate a request 'submitting' the form - \codename\core\app::getRequest()->setData($formSentField->getProperty('field_name'), 1); - \codename\core\app::getRequest()->setData('testmodel_text', 'abc'); - - // create a new crud instance to simulate creation - // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error - $model = $this->getModel('testmodel'); - $saveCrudInstance = new \codename\core\ui\crud($model); - - $saveCrudInstance->eventCrudBeforeValidation->addEventHandler(new \codename\core\eventHandler(function(array $data) { - $data['testmodel_testmodeljoin_id'] = 'abc'; - return $data; - })); - - $this->assertEmpty($saveCrudInstance->edit($id)); - - $this->assertEquals('save_error', \codename\core\app::getResponse()->getData('view')); - $errors = \codename\core\app::getResponse()->getData('errors'); - - $this->assertCount(1, $errors); - $this->assertEquals([ - [ - '__IDENTIFIER' => 'testmodel_testmodeljoin_id', - '__CODE' => 'VALIDATION.FIELD_INVALID', - '__TYPE' => 'VALIDATION', - '__DETAILS' => [ + /** + * [testCrudEditModelInvalid description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudEditModelInvalid(): void + { + $model = $this->getModel('testmodel'); + + // insert an entry and pass on PKEY for further 'editing' + $model->save([ + 'testmodel_text' => 'XYZ', + ]); + $id = $model->lastInsertId(); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->edit($id); + + // renderer? + static::assertEquals('frontend/form/compact/form', app::getResponse()->getData('form')); + + $form = $crudInstance->getForm(); + + static::assertInstanceOf(form::class, $form); + + $formSentField = null; + foreach ($form->getFields() as $field) { + // detect form sent field + if (str_starts_with($field->getConfig()->get('field_name'), 'formSent')) { + $formSentField = $field; + } + } + + // emulate a request 'submitting' the form + app::getRequest()->setData($formSentField->getConfig()->get('field_name'), 1); + app::getRequest()->setData('testmodel_text', 'abc'); + + // create a new crud instance to simulate creation + // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error + $model = $this->getModel('testmodel'); + $saveCrudInstance = new crud($model); + + $saveCrudInstance->eventCrudBeforeValidation->addEventHandler( + new eventHandler(function (array $data) { + $data['testmodel_testmodeljoin_id'] = 'abc'; + return $data; + }) + ); + + try { + $saveCrudInstance->edit($id); + } catch (Exception) { + static::fail(); + } + + static::assertEquals('save_error', app::getResponse()->getData('view')); + $errors = app::getResponse()->getData('errors'); + + static::assertCount(1, $errors); + static::assertEquals([ [ - '__IDENTIFIER' => 'VALUE', - '__CODE' => 'VALIDATION.VALUE_NOT_A_NUMBER', - '__TYPE' => 'VALIDATION', - '__DETAILS' => 'abc', + '__IDENTIFIER' => 'testmodel_testmodeljoin_id', + '__CODE' => 'VALIDATION.FIELD_INVALID', + '__TYPE' => 'VALIDATION', + '__DETAILS' => [ + [ + '__IDENTIFIER' => 'VALUE', + '__CODE' => 'VALIDATION.VALUE_NOT_A_NUMBER', + '__TYPE' => 'VALIDATION', + '__DETAILS' => 'abc', + ], + ], ], - ], - ] - ], $errors); - } - - /** - * [testCrudEditValidationError description] - */ - public function testCrudEditValidationError(): void { - $model = $this->getModel('testmodel'); - - // insert an entry and pass on PKEY for further 'editing' - $model->save([ - 'testmodel_text' => 'XYZ' - ]); - $id = $model->lastInsertId(); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->edit($id); - - // renderer? - $this->assertEquals('frontend/form/compact/form', \codename\core\app::getResponse()->getData('form')); - - $form = $crudInstance->getForm(); - - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - $formSentField = null; - foreach($form->getFields() as $field) { - // detect form sent field - if(strpos($field->getProperty('field_name'), 'formSent') === 0) { - $formSentField = $field; - } + ], $errors); } - // emulate a request 'submitting' the form - \codename\core\app::getRequest()->setData($formSentField->getProperty('field_name'), 1); - \codename\core\app::getRequest()->setData('testmodel_text', 'abc'); - - // create a new crud instance to simulate creation - // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error - $model = $this->getModel('testmodel'); - $saveCrudInstance = new \codename\core\ui\crud($model); - - $saveCrudInstance->eventCrudValidation->addEventHandler(new \codename\core\eventHandler(function($data) { - $errors = new \codename\core\errorstack('VALIDATION'); - $errors->addError('EXAMPLE', 'EXAMPLE', 'EXAMPLE'); - - return $errors->getErrors(); - })); - - $this->assertEmpty($saveCrudInstance->edit($id)); - - $this->assertEquals('save_error', \codename\core\app::getResponse()->getData('view')); - $errors = \codename\core\app::getResponse()->getData('errors'); + /** + * [testCrudEditValidationError description] + * @throws ReflectionException + * @throws DateMalformedStringException + * @throws \codename\core\exception + */ + public function testCrudEditValidationError(): void + { + $model = $this->getModel('testmodel'); + + // insert an entry and pass on PKEY for further 'editing' + $model->save([ + 'testmodel_text' => 'XYZ', + ]); + $id = $model->lastInsertId(); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->edit($id); + + // renderer? + static::assertEquals('frontend/form/compact/form', app::getResponse()->getData('form')); + + $form = $crudInstance->getForm(); + + static::assertInstanceOf(form::class, $form); + + $formSentField = null; + foreach ($form->getFields() as $field) { + // detect form sent field + if (str_starts_with($field->getConfig()->get('field_name'), 'formSent')) { + $formSentField = $field; + } + } + + // emulate a request 'submitting' the form + app::getRequest()->setData($formSentField->getConfig()->get('field_name'), 1); + app::getRequest()->setData('testmodel_text', 'abc'); + + // create a new crud instance to simulate creation + // NOTE: reset the model to avoid the EXCEPTION_MODEL_SCHEMATIC_SQL_CHILDREN_AMBIGUOUS_JOINS error + $model = $this->getModel('testmodel'); + $saveCrudInstance = new crud($model); + + $saveCrudInstance->eventCrudValidation->addEventHandler( + new eventHandler(function ($data) { + $errors = new errorstack('VALIDATION'); + $errors->addError('EXAMPLE', 'EXAMPLE', 'EXAMPLE'); + + return $errors->getErrors(); + }) + ); + + try { + $saveCrudInstance->edit($id); + } catch (Exception) { + static::fail(); + } + + static::assertEquals('save_error', app::getResponse()->getData('view')); + $errors = app::getResponse()->getData('errors'); + + static::assertCount(1, $errors); + static::assertEquals([ + [ + '__IDENTIFIER' => 'EXAMPLE', + '__CODE' => 'VALIDATION.EXAMPLE', + '__TYPE' => 'VALIDATION', + '__DETAILS' => 'EXAMPLE', + ], + ], $errors); + } - $this->assertCount(1, $errors); - $this->assertEquals([ - [ - '__IDENTIFIER' => 'EXAMPLE', - '__CODE' => 'VALIDATION.EXAMPLE', - '__TYPE' => 'VALIDATION', - '__DETAILS' => 'EXAMPLE', - ] - ], $errors); - } + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws \codename\core\exception + */ + protected function tearDown(): void + { + $this->getModel('testmodel') + ->addFilter('testmodel_id', 0, '>') + ->delete(); + + $this->getModel('testmodeljoin') + ->addFilter('testmodeljoin_id', 0, '>') + ->delete(); + + $this->getModel('testmodelforcejoin') + ->addFilter('testmodelforcejoin_id', 0, '>') + ->delete(); + + $this->getModel('testmodelcollection') + ->addFilter('testmodelcollection_id', 0, '>') + ->delete(); + } + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws ErrorException + * @throws Throwable + * @throws compileErrorException + * @throws coreErrorException + * @throws coreWarningException + * @throws parseException + * @throws recoverableErrorException + * @throws strictException + * @throws userErrorException + * @throws userWarningException + * @throws warningException + * @throws \codename\core\exception + */ + protected function setUp(): void + { + overrideableApp::resetRequest(); + overrideableApp::resetResponse(); + $app = static::createApp(); + + // Additional overrides to get a more complete app lifecycle + // and allow static global app::getModel() to work correctly + $app::__setApp('crudtest'); + $app::__setVendor('codename'); + $app::__setNamespace('\\codename\\core\\ui\\tests\\crud'); + $app::__setHomedir(__DIR__); + + $app::getAppstack(); + + // avoid re-init + if (static::$initialized) { + return; + } + + static::$initialized = true; + + static::setEnvironmentConfig([ + 'test' => [ + 'database' => [ + // NOTE: by default, we do these tests using + // pure in-memory sqlite. + 'default' => [ + 'driver' => 'sqlite', + // 'database_file' => 'testmodel.sqlite', + 'database_file' => ':memory:', + ], + ], + 'cache' => [ + 'default' => [ + 'driver' => 'memory', + ], + ], + 'filesystem' => [ + 'local' => [ + 'driver' => 'local', + ], + ], + 'translate' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + 'templateengine' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + 'log' => [ + 'default' => [ + 'driver' => 'system', + 'data' => [ + 'name' => 'dummy', + ], + ], + ], + ], + ]); + + static::createModel( + 'crudtest', + 'testmodel', + testmodel::$staticConfig, + function ($schema, $model, $config) { + return new testmodel([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodeljoin', + testmodeljoin::$staticConfig, + function ($schema, $model, $config) { + return new testmodeljoin([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelforcejoin', + testmodelforcejoin::$staticConfig, + function ($schema, $model, $config) { + return new testmodelforcejoin([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelcollection', + testmodelcollection::$staticConfig, + function ($schema, $model, $config) { + return new testmodelcollection([]); + } + ); + + static::architect('crudtest', 'codename', 'test'); + } } diff --git a/tests/crud/crudListTest.php b/tests/crud/crudListTest.php index 7106005..4605c97 100644 --- a/tests/crud/crudListTest.php +++ b/tests/crud/crudListTest.php @@ -1,1009 +1,1203 @@ getModel('testmodel'); + $crudInstance = new crud($model); + + $crudInstance->addTopaction([ + 'name' => 'exampleTopName', + 'view' => 'exampleTopView', + 'context' => 'exampleTopContext', + 'icon' => 'exampleTopIcon', + 'btnClass' => 'exampleTopBtnClass', + ]); + + $crudInstance->addBulkaction([ + 'name' => 'exampleBulkName', + 'view' => 'exampleBulkView', + 'context' => 'exampleBulkContext', + 'icon' => 'exampleBulkIcon', + 'btnClass' => 'exampleBulkBtnClass', + '_security' => [ + 'group' => 'example', + ], + ]); + + $crudInstance->addElementaction([ + 'name' => 'exampleElementName', + 'view' => 'exampleElementView', + 'context' => 'exampleElementContext', + 'icon' => 'exampleElementIcon', + 'btnClass' => 'exampleElementBtnClass', + 'condition' => '$condition = false;', + ]); + + $crudInstance->addModifier('example', function ($row) { + return 'example'; + }); + + try { + $crudInstance->listconfig(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertEquals([ + 'exampleTopName' => [ + 'name' => 'exampleTopName', + 'view' => 'exampleTopView', + 'context' => 'exampleTopContext', + 'icon' => 'exampleTopIcon', + 'btnClass' => 'exampleTopBtnClass', + 'display' => 'BTN_EXAMPLETOPNAME', + ], + ], $responseData['topActions']); + static::assertEmpty($responseData['bulkActions']); + static::assertEmpty($responseData['elementActions']); + + static::assertEquals([ + 'testmodel_text', + 'testmodel_testmodeljoin_id', + 'testmodel_id', + 'example', + ], $responseData['visibleFields']); + + static::assertInstanceOf(form::class, $responseData['filterform']); + } + + /** + * [testCrudListConfigWithSeparateConfig description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListConfigWithSeparateConfig(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + $crudInstance->onCreateFormfield = function (array &$fielddata) { + $fielddata['field_example'] = 'example'; + }; + $crudInstance->onFormfieldCreated = function (field $field) { + if ($field->getConfig()->get('field_example') === 'example') { + $field->setProperty('field_example', 'example2'); + } + }; + + $crudInstance->setConfig('crudtest_testmodel_crudlistconfig'); + + $crudInstance->addTopaction([ + 'name' => 'exampleTopName', + 'view' => 'exampleTopView', + 'context' => 'exampleTopContext', + 'icon' => 'exampleTopIcon', + 'btnClass' => 'exampleTopBtnClass', + ]); + + $crudInstance->addBulkaction([ + 'name' => 'exampleBulkName', + 'view' => 'exampleBulkView', + 'context' => 'exampleBulkContext', + 'icon' => 'exampleBulkIcon', + 'btnClass' => 'exampleBulkBtnClass', + '_security' => [ + 'group' => 'example', + ], + ]); + + $crudInstance->addElementaction([ + 'name' => 'exampleElementName', + 'view' => 'exampleElementView', + 'context' => 'exampleElementContext', + 'icon' => 'exampleElementIcon', + 'btnClass' => 'exampleElementBtnClass', + 'condition' => '$condition = false;', + ]); + + $customizedFields = $crudInstance->getConfig()->get('customized_fields'); + static::assertEquals([ + 'testmodel_testmodeljoin_id', + ], $customizedFields); + $crudInstance->setCustomizedFields($customizedFields); + + $crudInstance->addModifier('example', function ($row) { + return 'example'; + }); + + $crudInstance->setColumnOrder([ + 'testmodel_id', + 'testmodel_text', + ]); + + try { + $crudInstance->listconfig(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertEquals([ + 'exampleTopName' => [ + 'name' => 'exampleTopName', + 'view' => 'exampleTopView', + 'context' => 'exampleTopContext', + 'icon' => 'exampleTopIcon', + 'btnClass' => 'exampleTopBtnClass', + 'display' => 'BTN_EXAMPLETOPNAME', + ], + ], $responseData['topActions']); + static::assertEmpty($responseData['bulkActions']); + static::assertEmpty($responseData['elementActions']); + + static::assertEquals([ + 'testmodel_id', + 'testmodel_text', + 'testmodel_testmodeljoin_id', + 'example', + ], $responseData['visibleFields']); + + static::assertInstanceOf(form::class, $responseData['filterform']); + + $fields = $responseData['filterform']->getFields(); + static::assertCount(3, $fields); + + // only by foreign fields + static::assertEquals('example2', $fields[1]->getProperty('field_example')); + } + + /** + * [testCrudListConfigDisplaySelectedFields description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListConfigDisplaySelectedFields(): void + { + app::getRequest()->setData('display_selectedfields', ['testmodel_text']); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + try { + $crudInstance->listconfig(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertEquals([ + 'testmodel_text', + 'testmodel_id', + ], $responseData['visibleFields']); + } + + /** + * [testCrudListConfigDisplaySelectedFieldsWithForceJoin description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListConfigDisplaySelectedFieldsWithForceJoin(): void + { + app::getRequest()->setData('display_selectedfields', ['testmodelforcejoin_text']); + + $model = $this->getModel('testmodelforcejoin'); + $crudInstance = new crud($model); + + try { + $crudInstance->listconfig(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertEquals([ + 'testmodelforcejoin_text', + 'testmodelforcejoin_id', + ], $responseData['visibleFields']); + } + + /** + * [testCrudListConfigImportAndExport description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListConfigImportAndExport(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->setConfig('crudtest_testmodel_crudlistconfig'); + + try { + $crudInstance->listconfig(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertTrue($responseData['enable_import']); + static::assertTrue($responseData['enable_export']); + static::assertEquals([ + 'json', + ], $responseData['export_types']); + } + + /** + * [testCrudListView description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListView(): void + { + // set demo data + $model = $this->getModel('testmodel')->addModel($joinedModel = $this->getModel('testmodeljoin')); + if (!($model instanceof sql)) { + static::fail('setup fail'); + } + $model->saveWithChildren([ + 'testmodel_text' => 'moep', + 'testmodel_testmodeljoin' => [ + 'testmodeljoin_text' => 'se', + ], + ]); + + $rootId = $model->lastInsertId(); + $joinedId = $joinedModel->lastInsertId(); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + $crudInstance->addModifier('example', function ($row) { + return 'example'; + }); + + $crudInstance->addResultsetModifier(function ($results) { + foreach ($results as &$result) { + $result['testmodel_text'] .= $result['testmodel_testmodeljoin']['testmodeljoin_text']; + } + return $results; + }); + + $crudInstance->addRowModifier(function ($row) { + return [ + 'example1' => 'omfg!', + 'example2' => true, + ]; + }); + + try { + $crudInstance->listview(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertEquals([ + 'testmodel_text', + 'testmodel_testmodeljoin_id', + 'testmodel_id', + 'example', + ], $responseData['visibleFields']); + + static::assertInstanceOf(form::class, $responseData['filterform']); + + static::assertEquals([ + [ + 'testmodel_text' => 'moepse', + 'testmodel_testmodeljoin_id_FORMATTED' => 'se', + 'testmodel_testmodeljoin_id' => $joinedId, + 'testmodel_id' => $rootId, + 'example' => 'example', + '__modifier' => 'example1="omfg!" example2', + ], + ], $responseData['rows']); + + static::assertEquals('example1="omfg!" example2', $responseData['rows'][0]['__modifier']); + + static::assertEquals([ + 'crud_pagination_seek_enabled' => false, + 'crud_pagination_count' => 1, + 'crud_pagination_page' => 1, + 'crud_pagination_pages' => 1.0, + 'crud_pagination_limit' => 5, + ], [ + 'crud_pagination_seek_enabled' => $responseData['crud_pagination_seek_enabled'], + 'crud_pagination_count' => $responseData['crud_pagination_count'], + 'crud_pagination_page' => $responseData['crud_pagination_page'], + 'crud_pagination_pages' => $responseData['crud_pagination_pages'], + 'crud_pagination_limit' => $responseData['crud_pagination_limit'], + ]); + } + + /** + * [testCrudListViewSmall description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListViewSmall(): void + { + // set demo data + $model = $this->getModel('testmodeljoin'); + $model->save([ + 'testmodeljoin_text' => 'example', + ]); + + $model = $this->getModel('testmodeljoin'); + $crudInstance = new crud($model); + + try { + $crudInstance->listview(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertEquals([ + 'testmodeljoin_text', + 'testmodeljoin_id', + ], $responseData['visibleFields']); + + static::assertCount(1, $responseData['rows']); + static::assertEquals('example', $responseData['rows'][0]['testmodeljoin_text']); + } + + /** + * [testCrudListViewSmallWithRowModifier description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListViewSmallWithRowModifier(): void + { + // set demo data + $model = $this->getModel('testmodeljoin'); + $model->save([ + 'testmodeljoin_text' => 'example', + ]); + + $model = $this->getModel('testmodeljoin'); + $crudInstance = new crud($model); + $crudInstance->addRowModifier(function ($row) { + return [ + 'example1' => 'omfg!', + 'example2' => true, + ]; + }); + + try { + $crudInstance->listview(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertEquals([ + 'testmodeljoin_text', + 'testmodeljoin_id', + ], $responseData['visibleFields']); + + static::assertCount(1, $responseData['rows']); + static::assertEquals('example', $responseData['rows'][0]['testmodeljoin_text']); + static::assertEquals('example1="omfg!" example2', $responseData['rows'][0]['__modifier']); + } + + /** + * [testCrudListViewWithSetResultData description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListViewWithSetResultData(): void + { + $model = $this->getModel('testmodeljoin'); + $model->save([ + 'testmodeljoin_text' => 'example', + ]); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model, null, 'crudtest_testmodel_field_is_array'); + $crudInstance->setProvideRawData(true); + $crudInstance->setResultData([ + [ + 'testmodel_text' => [ + 'example' => 'example', + ], + 'testmodel_testmodeljoin_id' => [1, 2, 3, 4, 5], + ], + ]); + + try { + $crudInstance->listview(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertEquals([ + ['testmodel_text', 'example'], + 'testmodel_testmodeljoin_id', + 'testmodel_id', + ], $responseData['visibleFields']); + + static::assertEquals([ + [ + 'testmodel_text' => [ + 'example' => 'example', + ], + 'testmodel_testmodeljoin_id' => [1, 2, 3, 4, 5], + 'testmodel_testmodeljoin_id_FORMATTED' => 'example', + 'testmodel_id' => null, + ], + ], $responseData['rows'], json_encode($responseData['rows'])); + } + + /** + * [testCrudListViewWithSeparateConfig description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListViewWithSeparateConfig(): void + { + app::getRequest()->setData('crud_pagination_page', 10); + app::getRequest()->setData('crud_pagination_limit', 10); + + // set demo data + $model = $this->getModel('testmodel')->addModel($joinedModel = $this->getModel('testmodeljoin')); + if (!($model instanceof sql)) { + static::fail('setup fail'); + } + $model->saveWithChildren([ + 'testmodel_text' => 'moep', + 'testmodel_testmodeljoin' => [ + 'testmodeljoin_text' => 'se', + ], + ]); + + $rootId = $model->lastInsertId(); + $joinedId = $joinedModel->lastInsertId(); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->setConfig('crudtest_testmodel_crudlistconfig'); + + $crudInstance->addModifier('example', function ($row) { + return 'example'; + }); + + $crudInstance->setColumnOrder([ + 'testmodel_id', + 'testmodel_text', + ]); + + $crudInstance->addResultsetModifier(function ($results) { + foreach ($results as &$result) { + $result['testmodel_text'] .= $result['testmodel_testmodeljoin']['testmodeljoin_text']; + } + return $results; + }); + + try { + $crudInstance->listview(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertEquals([ + 'testmodel_id', + 'testmodel_text', + 'testmodel_testmodeljoin_id', + 'example', + ], $responseData['visibleFields']); + + static::assertInstanceOf(form::class, $responseData['filterform']); + + $fields = $responseData['filterform']->getFields(); + static::assertCount(3, $fields); + static::assertInstanceOf(field::class, $fields[0]); + static::assertInstanceOf(field::class, $fields[1]); + static::assertInstanceOf(field::class, $fields[2]); + static::assertEquals('field_config_example_title', $fields[0]->getConfig()->get('field_title')); + + static::assertEquals([ + [ + 'testmodel_id' => $rootId, + 'testmodel_text' => 'moepse', + 'testmodel_testmodeljoin_id_FORMATTED' => 'se', + 'testmodel_testmodeljoin_id' => $joinedId, + 'example' => 'example', + ], + ], $responseData['rows']); + + static::assertEquals([ + 'crud_pagination_seek_enabled' => false, + 'crud_pagination_count' => 1, + 'crud_pagination_page' => 1, + 'crud_pagination_pages' => 1.0, + 'crud_pagination_limit' => 10, + ], [ + 'crud_pagination_seek_enabled' => $responseData['crud_pagination_seek_enabled'], + 'crud_pagination_count' => $responseData['crud_pagination_count'], + 'crud_pagination_page' => $responseData['crud_pagination_page'], + 'crud_pagination_pages' => $responseData['crud_pagination_pages'], + 'crud_pagination_limit' => $responseData['crud_pagination_limit'], + ]); + } + + /** + * [testCrudListViewDisplaySelectedFields description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListViewDisplaySelectedFields(): void + { + app::getRequest()->setData('display_selectedfields', ['testmodel_text']); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + try { + $crudInstance->listview(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertEquals([ + 'testmodel_text', + 'testmodel_id', + ], $responseData['visibleFields']); + } -class crudListTest extends base { - - /** - * [protected description] - * @var bool - */ - protected static $initialized = false; - - /** - * @inheritDoc - */ - public static function tearDownAfterClass(): void - { - parent::tearDownAfterClass(); - static::$initialized = false; - } - - /** - * @inheritDoc - */ - protected function tearDown(): void - { - $this->getModel('testmodel') - ->addFilter('testmodel_id', 0, '>') - ->delete(); - - $this->getModel('testmodeljoin') - ->addFilter('testmodeljoin_id', 0, '>') - ->delete(); - - $this->getModel('testmodelforcejoin') - ->addFilter('testmodelforcejoin_id', 0, '>') - ->delete(); - - $this->getModel('testmodelcollection') - ->addFilter('testmodelcollection_id', 0, '>') - ->delete(); - } - - /** - * @inheritDoc - */ - protected function setUp(): void - { - overrideableApp::resetRequest(); - overrideableApp::resetResponse(); - $app = static::createApp(); - - // Additional overrides to get a more complete app lifecycle - // and allow static global app::getModel() to work correctly - $app->__setApp('crudtest'); - $app->__setVendor('codename'); - $app->__setNamespace('\\codename\\core\\ui\\tests\\crud'); - $app->__setHomedir(__DIR__); - - $app->getAppstack(); - - // avoid re-init - if(static::$initialized) { - return; + /** + * [testCrudListViewImportAndExport description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListViewImportAndExport(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->setConfig('crudtest_testmodel_crudlistconfig'); + + try { + $crudInstance->listview(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertTrue($responseData['enable_import']); + static::assertTrue($responseData['enable_export']); + static::assertEquals([ + 'json', + ], $responseData['export_types']); } - // TODO: rework via overrideableApp - $app->__injectClientInstance('auth', 'default', new dummyAuth); + /** + * [testCrudListViewCrudEditable description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListViewCrudEditable(): void + { + app::getRequest()->setData('crud_editable', true); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + try { + $crudInstance->listview(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertEquals([ + 'testmodel_text', + 'testmodel_testmodeljoin_id', + 'testmodel_id', + ], $responseData['visibleFields']); + } + + /** + * [testCrudListViewSeekStablePosition description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListViewSeekStablePosition(): void + { + // set demo data + $model = $this->getModel('testmodel'); + if (!($model instanceof sql)) { + static::fail('setup fail'); + } + $model->saveWithChildren([ + 'testmodel_text' => 'moepse1', + 'testmodel_flag' => 1, + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'moepse2', + 'testmodel_flag' => 2, + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'moepse3', + 'testmodel_flag' => 4, + ]); + + app::getRequest()->setData('crud_pagination_first_id', 1); + app::getRequest()->setData('crud_pagination_seek', 0); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->setConfig('crudtest_testmodel_seek'); + + try { + $crudInstance->listview(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertCount(2, $responseData['rows']); + static::assertEquals('moepse1', $responseData['rows'][0]['testmodel_text']); + static::assertEquals(1, $responseData['rows'][0]['testmodel_flag']); + static::assertEquals('moepse2', $responseData['rows'][1]['testmodel_text']); + static::assertEquals(2, $responseData['rows'][1]['testmodel_flag']); + } + + /** + * [testCrudListViewSeekMovingBackwards description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListViewSeekMovingBackwards(): void + { + // set demo data + $model = $this->getModel('testmodel'); + if (!($model instanceof sql)) { + static::fail('setup fail'); + } + $model->saveWithChildren([ + 'testmodel_text' => 'moepse1', + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'moepse2', + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'moepse3', + ]); + + $model->addFilter('testmodel_text', 'moepse3'); + $res = $model->search()->getResult(); + + app::getRequest()->setData('crud_pagination_first_id', $res[0]['testmodel_id']); + app::getRequest()->setData('crud_pagination_seek', -1); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->setConfig('crudtest_testmodel_seek'); + + try { + $crudInstance->listview(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertCount(2, $responseData['rows']); + static::assertEquals('moepse1', $responseData['rows'][0]['testmodel_text']); + static::assertEquals('moepse2', $responseData['rows'][1]['testmodel_text']); + } + + /** + * [testCrudListViewSeekMovingForwards description] + * @throws ReflectionException + * @throws DateMalformedStringException + * @throws \codename\core\exception + */ + public function testCrudListViewSeekMovingForwards(): void + { + // set demo data + $model = $this->getModel('testmodel'); + if (!($model instanceof sql)) { + static::fail('setup fail'); + } + $model->saveWithChildren([ + 'testmodel_text' => 'moepse1', + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'moepse2', + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'moepse3', + ]); + + $model->addFilter('testmodel_text', 'moepse2'); + $res = $model->search()->getResult(); + + app::getRequest()->setData('crud_pagination_last_id', $res[0]['testmodel_id']); + app::getRequest()->setData('crud_pagination_seek', 1); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->setConfig('crudtest_testmodel_seek'); + + try { + $crudInstance->listview(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertCount(1, $responseData['rows']); + static::assertEquals('moepse3', $responseData['rows'][0]['testmodel_text']); + } - static::$initialized = true; + /** + * [testCrudListViewPaginationWithFilter description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListViewPaginationWithFilter(): void + { + app::getRequest()->setData('crud_pagination_page_prev', 2); + app::getRequest()->setData('crud_pagination_limit', 2); + + // set demo data + $model = $this->getModel('testmodel'); + if (!($model instanceof sql)) { + static::fail('setup fail'); + } + $model->saveWithChildren([ + 'testmodel_text' => 'example1', + 'testmodel_flag' => 1, + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'example2', + 'testmodel_flag' => 1, + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'example3', + 'testmodel_flag' => 1, + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'example4', + 'testmodel_flag' => 1, + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'example5', + 'testmodel_flag' => 1, + ]); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + app::getRequest()->setData($crudInstance::CRUD_FILTER_IDENTIFIER, [ + 'testmodel_flag' => 1, + ]); + + try { + $crudInstance->listview(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertEquals([ + 'testmodel_text', + 'testmodel_testmodeljoin_id', + 'testmodel_id', + ], $responseData['visibleFields']); + + static::assertCount(2, $responseData['rows']); + static::assertEquals('example3', $responseData['rows'][0]['testmodel_text']); + static::assertEquals('example4', $responseData['rows'][1]['testmodel_text']); + + static::assertEquals([ + 'crud_pagination_seek_enabled' => false, + 'crud_pagination_count' => 5, + 'crud_pagination_page' => 2, + 'crud_pagination_pages' => 3.0, + 'crud_pagination_limit' => 2, + ], [ + 'crud_pagination_seek_enabled' => $responseData['crud_pagination_seek_enabled'], + 'crud_pagination_count' => $responseData['crud_pagination_count'], + 'crud_pagination_page' => $responseData['crud_pagination_page'], + 'crud_pagination_pages' => $responseData['crud_pagination_pages'], + 'crud_pagination_limit' => $responseData['crud_pagination_limit'], + ]); + } - static::setEnvironmentConfig([ - 'test' => [ - 'database' => [ - // NOTE: by default, we do these tests using - // pure in-memory sqlite. - 'default' => [ - 'driver' => 'sqlite', - // 'database_file' => 'testmodel.sqlite', - 'database_file' => ':memory:', + /** + * [testCrudListViewFilter description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testCrudListViewFilter(): void + { + app::getRequest()->setData('crud_pagination_page_prev', 2); + app::getRequest()->setData('crud_pagination_limit', 2); + + // set demo data + $model = $this->getModel('testmodel'); + if (!($model instanceof sql)) { + static::fail('setup fail'); + } + $model->saveWithChildren([ + 'testmodel_text' => 'example1', + 'testmodel_number_natural' => 1, + 'testmodel_flag' => 3, + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'example2', + 'testmodel_number_natural' => 1, + 'testmodel_flag' => 3, + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'example3', + 'testmodel_number_natural' => 1, + 'testmodel_flag' => 3, + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'example4', + 'testmodel_number_natural' => 1, + 'testmodel_flag' => 3, + ]); + $model->saveWithChildren([ + 'testmodel_text' => 'example5', + 'testmodel_number_natural' => 1, + 'testmodel_flag' => 3, + ]); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->setConfig('crudtest_testmodel_filter'); + + // added filters + $now = new DateTime('now'); + app::getRequest()->setData($crudInstance::CRUD_FILTER_IDENTIFIER, [ + 'provide_filter_example' => true, + 'testmodel_flag' => [1, 2], + 'testmodel_text' => [ + 'example2', + 'example3', + 'example4', + 'example5', + ], + 'testmodel_number_natural' => 1, + 'search' => 'example%', + 'testmodel_created' => [ + $now->format('Y-m-d 00:00:00'), + $now->format('Y-m-d 23:59:59'), ], - ], - 'auth' => [ - 'default' => [ - 'driver' => 'dummy' - ] - ], - 'cache' => [ - 'default' => [ - 'driver' => 'memory' - ] - ], - 'filesystem' =>[ - 'local' => [ - 'driver' => 'local', - ] - ], - 'translate' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - 'templateengine' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - 'log' => [ - 'default' => [ - 'driver' => 'system', - 'data' => [ - 'name' => 'dummy' - ] - ] - ], - ] - ]); - - static::createModel( - 'crudtest', 'testmodel', - \codename\core\ui\tests\crud\model\testmodel::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodel([]); - } - ); - - static::createModel( - 'crudtest', 'testmodeljoin', - \codename\core\ui\tests\crud\model\testmodeljoin::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodeljoin([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelforcejoin', - \codename\core\ui\tests\crud\model\testmodelforcejoin::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelforcejoin([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelcollection', - \codename\core\ui\tests\crud\model\testmodelcollection::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelcollection([]); - } - ); - - static::architect('crudtest', 'codename', 'test'); - } - - /** - * [testCrudListConfig description] - */ - public function testCrudListConfig(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - - $crudInstance->addTopaction([ - 'name' => 'exampleTopName', - 'view' => 'exampleTopView', - 'context' => 'exampleTopContext', - 'icon' => 'exampleTopIcon', - 'btnClass' => 'exampleTopBtnClass', - ]); - - $crudInstance->addBulkaction([ - 'name' => 'exampleBulkName', - 'view' => 'exampleBulkView', - 'context' => 'exampleBulkContext', - 'icon' => 'exampleBulkIcon', - 'btnClass' => 'exampleBulkBtnClass', - '_security' => [ - 'group' => 'example' - ], - ]); - - $crudInstance->addElementaction([ - 'name' => 'exampleElementName', - 'view' => 'exampleElementView', - 'context' => 'exampleElementContext', - 'icon' => 'exampleElementIcon', - 'btnClass' => 'exampleElementBtnClass', - 'condition' => '$condition = false;' - ]); - - $crudInstance->addModifier('example', function($row) { - return 'example'; - }); - - $resultConfig = $crudInstance->listconfig(); - $this->assertEmpty($resultConfig); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertEquals([ - 'exampleTopName' => [ - 'name' => 'exampleTopName', - 'view' => 'exampleTopView', - 'context' => 'exampleTopContext', - 'icon' => 'exampleTopIcon', - 'btnClass' => 'exampleTopBtnClass', - 'display' => 'BTN_EXAMPLETOPNAME', - ] - ], $responseData['topActions']); - $this->assertEmpty($responseData['bulkActions']); - $this->assertEmpty($responseData['elementActions']); - - $this->assertEquals([ - 'testmodel_text', - 'testmodel_testmodeljoin_id', - 'testmodel_id', - 'example', - ], $responseData['visibleFields']); - - $this->assertInstanceOf(\codename\core\ui\form::class, $responseData['filterform']); - } - - /** - * [testCrudListConfigWithSeparateConfig description] - */ - public function testCrudListConfigWithSeparateConfig(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - - $crudInstance->onCreateFormfield = function(array &$fielddata) { - $fielddata['field_example'] = 'example'; - }; - $crudInstance->onFormfieldCreated = function(\codename\core\ui\field &$field) { - if($field->getProperty('field_example') === 'example') { - $field->setProperty('field_example', 'example2'); - } - }; - - $crudInstance->setConfig('crudtest_testmodel_crudlistconfig'); - - $crudInstance->addTopaction([ - 'name' => 'exampleTopName', - 'view' => 'exampleTopView', - 'context' => 'exampleTopContext', - 'icon' => 'exampleTopIcon', - 'btnClass' => 'exampleTopBtnClass', - ]); - - $crudInstance->addBulkaction([ - 'name' => 'exampleBulkName', - 'view' => 'exampleBulkView', - 'context' => 'exampleBulkContext', - 'icon' => 'exampleBulkIcon', - 'btnClass' => 'exampleBulkBtnClass', - '_security' => [ - 'group' => 'example' - ], - ]); - - $crudInstance->addElementaction([ - 'name' => 'exampleElementName', - 'view' => 'exampleElementView', - 'context' => 'exampleElementContext', - 'icon' => 'exampleElementIcon', - 'btnClass' => 'exampleElementBtnClass', - 'condition' => '$condition = false;' - ]); - - $customizedFields = $crudInstance->getConfig()->get('customized_fields'); - $this->assertEquals([ - 'testmodel_testmodeljoin_id', - ], $customizedFields); - $crudInstance->setCustomizedFields($customizedFields); - - $crudInstance->addModifier('example', function($row) { - return 'example'; - }); - - $crudInstance->setColumnOrder([ - 'testmodel_id', - 'testmodel_text', - ]); - - $resultConfig = $crudInstance->listconfig(); - $this->assertEmpty($resultConfig); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertEquals([ - 'exampleTopName' => [ - 'name' => 'exampleTopName', - 'view' => 'exampleTopView', - 'context' => 'exampleTopContext', - 'icon' => 'exampleTopIcon', - 'btnClass' => 'exampleTopBtnClass', - 'display' => 'BTN_EXAMPLETOPNAME', - ] - ], $responseData['topActions']); - $this->assertEmpty($responseData['bulkActions']); - $this->assertEmpty($responseData['elementActions']); - - $this->assertEquals([ - 'testmodel_id', - 'testmodel_text', - 'testmodel_testmodeljoin_id', - 'example', - ], $responseData['visibleFields']); - - $this->assertInstanceOf(\codename\core\ui\form::class, $responseData['filterform']); - - $fields = $responseData['filterform']->getFields(); - $this->assertCount(3, $fields); - - // only by foreign fields - $this->assertEquals('example2', $fields[1]->getProperty('field_example')); - - } - - /** - * [testCrudListConfigDisplaySelectedFields description] - */ - public function testCrudListConfigDisplaySelectedFields(): void { - \codename\core\app::getRequest()->setData('display_selectedfields', [ 'testmodel_text' ]); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $resultConfig = $crudInstance->listconfig(); - $this->assertEmpty($resultConfig); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertEquals([ - 'testmodel_text', - 'testmodel_id', - ], $responseData['visibleFields']); - - } - - /** - * [testCrudListConfigDisplaySelectedFieldsWithForceJoin description] - */ - public function testCrudListConfigDisplaySelectedFieldsWithForceJoin(): void { - \codename\core\app::getRequest()->setData('display_selectedfields', [ 'testmodelforcejoin_text' ]); - - $model = $this->getModel('testmodelforcejoin'); - $crudInstance = new \codename\core\ui\crud($model); - $resultConfig = $crudInstance->listconfig(); - $this->assertEmpty($resultConfig); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertEquals([ - 'testmodelforcejoin_text', - 'testmodelforcejoin_id', - ], $responseData['visibleFields']); - - } - - /** - * [testCrudListConfigImportAndExport description] - */ - public function testCrudListConfigImportAndExport(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->setConfig('crudtest_testmodel_crudlistconfig'); - $resultConfig = $crudInstance->listconfig(); - $this->assertEmpty($resultConfig); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertTrue($responseData['enable_import']); - $this->assertTrue($responseData['enable_export']); - $this->assertEquals([ - 'json', - ], $responseData['export_types']); - - } - - /** - * [testCrudListView description] - */ - public function testCrudListView(): void { - // set demo data - $model = $this->getModel('testmodel')->addModel($joinedModel = $this->getModel('testmodeljoin')); - $model->saveWithChildren([ - 'testmodel_text' => 'moep', - 'testmodel_testmodeljoin' => [ - 'testmodeljoin_text' => 'se', - ] - ]); - - $rootId = $model->lastInsertId(); - $joinedId = $joinedModel->lastInsertId(); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - - $crudInstance->addModifier('example', function($row) { - return 'example'; - }); - - $crudInstance->addResultsetModifier(function($results) { - foreach($results as &$result) { - $result['testmodel_text'] .= $result['testmodel_testmodeljoin']['testmodeljoin_text']; - } - return $results; - }); - - $crudInstance->addRowModifier(function($row) { - return [ - 'example1' => 'omfg!', - 'example2' => true, - ]; - }); - - $resultView = $crudInstance->listview(); - $this->assertEmpty($resultView); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertEquals([ - 'testmodel_text', - 'testmodel_testmodeljoin_id', - 'testmodel_id', - 'example', - ], $responseData['visibleFields']); - - $this->assertInstanceOf(\codename\core\ui\form::class, $responseData['filterform']); - - $this->assertEquals([ - [ - 'testmodel_text' => 'moepse', - 'testmodel_testmodeljoin_id_FORMATTED' => 'se', - 'testmodel_testmodeljoin_id' => $joinedId, - 'testmodel_id' => $rootId, - 'example' => 'example', - '__modifier' => 'example1="omfg!" example2', - ], - ], $responseData['rows']); - - $this->assertEquals('example1="omfg!" example2', $responseData['rows'][0]['__modifier']); - - $this->assertEquals([ - 'crud_pagination_seek_enabled' => false, - 'crud_pagination_count' => 1, - 'crud_pagination_page' => 1, - 'crud_pagination_pages' => 1.0, - 'crud_pagination_limit' => 5, - ], [ - 'crud_pagination_seek_enabled' => $responseData['crud_pagination_seek_enabled'], - 'crud_pagination_count' => $responseData['crud_pagination_count'], - 'crud_pagination_page' => $responseData['crud_pagination_page'], - 'crud_pagination_pages' => $responseData['crud_pagination_pages'], - 'crud_pagination_limit' => $responseData['crud_pagination_limit'], - ]); - } - - /** - * [testCrudListViewSmall description] - */ - public function testCrudListViewSmall(): void { - // set demo data - $model = $this->getModel('testmodeljoin'); - $model->save([ - 'testmodeljoin_text' => 'example', - ]); - - $model = $this->getModel('testmodeljoin'); - $crudInstance = new \codename\core\ui\crud($model); - $resultView = $crudInstance->listview(); - $this->assertEmpty($resultView); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertEquals([ - 'testmodeljoin_text', - 'testmodeljoin_id', - ], $responseData['visibleFields']); - - $this->assertCount(1, $responseData['rows']); - $this->assertEquals('example', $responseData['rows'][0]['testmodeljoin_text']); - - } - - /** - * [testCrudListViewSmallWithRowModifier description] - */ - public function testCrudListViewSmallWithRowModifier(): void { - // set demo data - $model = $this->getModel('testmodeljoin'); - $model->save([ - 'testmodeljoin_text' => 'example', - ]); - - $model = $this->getModel('testmodeljoin'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->addRowModifier(function($row) { - return [ - 'example1' => 'omfg!', - 'example2' => true, - ]; - }); - $resultView = $crudInstance->listview(); - $this->assertEmpty($resultView); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertEquals([ - 'testmodeljoin_text', - 'testmodeljoin_id', - ], $responseData['visibleFields']); - - $this->assertCount(1, $responseData['rows']); - $this->assertEquals('example', $responseData['rows'][0]['testmodeljoin_text']); - $this->assertEquals('example1="omfg!" example2', $responseData['rows'][0]['__modifier']); - - } - - /** - * [testCrudListViewWithSetResultData description] - */ - public function testCrudListViewWithSetResultData(): void { - $model = $this->getModel('testmodeljoin'); - $model->save([ - 'testmodeljoin_text' => 'example', - ]); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model, null, 'crudtest_testmodel_field_is_array'); - $crudInstance->setProvideRawData(true); - $crudInstance->setResultData([ - [ - 'testmodel_text' => [ - 'example' => 'example', - ], - 'testmodel_testmodeljoin_id' => [ 1, 2, 3, 4, 5 ], - ], - ]); - $resultView = $crudInstance->listview(); - $this->assertEmpty($resultView); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertEquals([ - [ 'testmodel_text', 'example' ], - 'testmodel_testmodeljoin_id', - 'testmodel_id', - ], $responseData['visibleFields']); - - $this->assertEquals([ - [ - 'testmodel_text' => [ - 'example' => 'example', - ], - 'testmodel_testmodeljoin_id' => [ 1, 2, 3, 4, 5 ], - 'testmodel_testmodeljoin_id_FORMATTED' => 'example', - 'testmodel_id' => null, - ], - ], $responseData['rows'], json_encode($responseData['rows'])); - } - - /** - * [testCrudListViewWithSeparateConfig description] - */ - public function testCrudListViewWithSeparateConfig(): void { - \codename\core\app::getRequest()->setData('crud_pagination_page', 10); - \codename\core\app::getRequest()->setData('crud_pagination_limit', 10); - - // set demo data - $model = $this->getModel('testmodel')->addModel($joinedModel = $this->getModel('testmodeljoin')); - $model->saveWithChildren([ - 'testmodel_text' => 'moep', - 'testmodel_testmodeljoin' => [ - 'testmodeljoin_text' => 'se', - ] - ]); - - $rootId = $model->lastInsertId(); - $joinedId = $joinedModel->lastInsertId(); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->setConfig('crudtest_testmodel_crudlistconfig'); - - $crudInstance->addModifier('example', function($row) { - return 'example'; - }); - - $crudInstance->setColumnOrder([ - 'testmodel_id', - 'testmodel_text', - ]); - - $crudInstance->addResultsetModifier(function($results) { - foreach($results as &$result) { - $result['testmodel_text'] .= $result['testmodel_testmodeljoin']['testmodeljoin_text']; - } - return $results; - }); - - $resultView = $crudInstance->listview(); - $this->assertEmpty($resultView); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertEquals([ - 'testmodel_id', - 'testmodel_text', - 'testmodel_testmodeljoin_id', - 'example', - ], $responseData['visibleFields']); - - $this->assertInstanceOf(\codename\core\ui\form::class, $responseData['filterform']); - - $fields = $responseData['filterform']->getFields(); - $this->assertCount(3, $fields); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[0]); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[1]); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[2]); - $this->assertEquals('field_config_example_title', $fields[0]->getConfig()->get('field_title')); - - $this->assertEquals([ - [ - 'testmodel_id' => $rootId, - 'testmodel_text' => 'moepse', - 'testmodel_testmodeljoin_id_FORMATTED' => 'se', - 'testmodel_testmodeljoin_id' => $joinedId, - 'example' => 'example', - ], - ], $responseData['rows']); - - $this->assertEquals([ - 'crud_pagination_seek_enabled' => false, - 'crud_pagination_count' => 1, - 'crud_pagination_page' => 1, - 'crud_pagination_pages' => 1.0, - 'crud_pagination_limit' => 10, - ], [ - 'crud_pagination_seek_enabled' => $responseData['crud_pagination_seek_enabled'], - 'crud_pagination_count' => $responseData['crud_pagination_count'], - 'crud_pagination_page' => $responseData['crud_pagination_page'], - 'crud_pagination_pages' => $responseData['crud_pagination_pages'], - 'crud_pagination_limit' => $responseData['crud_pagination_limit'], - ]); - } - - /** - * [testCrudListViewDisplaySelectedFields description] - */ - public function testCrudListViewDisplaySelectedFields(): void { - \codename\core\app::getRequest()->setData('display_selectedfields', [ 'testmodel_text' ]); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $resultConfig = $crudInstance->listview(); - $this->assertEmpty($resultConfig); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertEquals([ - 'testmodel_text', - 'testmodel_id', - ], $responseData['visibleFields']); - - } - - /** - * [testCrudListViewImportAndExport description] - */ - public function testCrudListViewImportAndExport(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->setConfig('crudtest_testmodel_crudlistconfig'); - $resultConfig = $crudInstance->listview(); - $this->assertEmpty($resultConfig); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertTrue($responseData['enable_import']); - $this->assertTrue($responseData['enable_export']); - $this->assertEquals([ - 'json', - ], $responseData['export_types']); - - } - - /** - * [testCrudListViewCrudEditable description] - */ - public function testCrudListViewCrudEditable(): void { - \codename\core\app::getRequest()->setData('crud_editable', true); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $resultConfig = $crudInstance->listview(); - $this->assertEmpty($resultConfig); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertEquals([ - 'testmodel_text', - 'testmodel_testmodeljoin_id', - 'testmodel_id', - ], $responseData['visibleFields']); - - } - - /** - * [testCrudListViewSeekStablePosition description] - */ - public function testCrudListViewSeekStablePosition(): void { - // set demo data - $model = $this->getModel('testmodel'); - $model->saveWithChildren([ - 'testmodel_text' => 'moepse1', - 'testmodel_flag' => 1, - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'moepse2', - 'testmodel_flag' => 2, - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'moepse3', - 'testmodel_flag' => 4, - ]); - - \codename\core\app::getRequest()->setData('crud_pagination_first_id', 1); - \codename\core\app::getRequest()->setData('crud_pagination_seek', 0); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->setConfig('crudtest_testmodel_seek'); - $resultConfig = $crudInstance->listview(); - $this->assertEmpty($resultConfig); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertCount(2, $responseData['rows']); - $this->assertEquals('moepse1', $responseData['rows'][0]['testmodel_text']); - $this->assertEquals(1, $responseData['rows'][0]['testmodel_flag']); - $this->assertEquals('moepse2', $responseData['rows'][1]['testmodel_text']); - $this->assertEquals(2, $responseData['rows'][1]['testmodel_flag']); - - } - - /** - * [testCrudListViewSeekMovingBackwards description] - */ - public function testCrudListViewSeekMovingBackwards(): void { - // set demo data - $model = $this->getModel('testmodel'); - $model->saveWithChildren([ - 'testmodel_text' => 'moepse1', - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'moepse2', - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'moepse3', - ]); - - $model->addFilter('testmodel_text', 'moepse3'); - $res = $model->search()->getResult(); - - \codename\core\app::getRequest()->setData('crud_pagination_first_id', $res[0]['testmodel_id']); - \codename\core\app::getRequest()->setData('crud_pagination_seek', -1); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->setConfig('crudtest_testmodel_seek'); - $resultConfig = $crudInstance->listview(); - $this->assertEmpty($resultConfig); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertCount(2, $responseData['rows']); - $this->assertEquals('moepse1', $responseData['rows'][0]['testmodel_text']); - $this->assertEquals('moepse2', $responseData['rows'][1]['testmodel_text']); - - } - - /** - * [testCrudListViewSeekMovingForwards description] - */ - public function testCrudListViewSeekMovingForwards(): void { - // set demo data - $model = $this->getModel('testmodel'); - $model->saveWithChildren([ - 'testmodel_text' => 'moepse1', - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'moepse2', - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'moepse3', - ]); - - $model->addFilter('testmodel_text', 'moepse2'); - $res = $model->search()->getResult(); - - \codename\core\app::getRequest()->setData('crud_pagination_last_id', $res[0]['testmodel_id']); - \codename\core\app::getRequest()->setData('crud_pagination_seek', 1); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->setConfig('crudtest_testmodel_seek'); - $resultConfig = $crudInstance->listview(); - $this->assertEmpty($resultConfig); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertCount(1, $responseData['rows']); - $this->assertEquals('moepse3', $responseData['rows'][0]['testmodel_text']); - - } - - /** - * [testCrudListViewPaginationWithFilter description] - */ - public function testCrudListViewPaginationWithFilter(): void { - \codename\core\app::getRequest()->setData('crud_pagination_page_prev', 2); - \codename\core\app::getRequest()->setData('crud_pagination_limit', 2); - - // set demo data - $model = $this->getModel('testmodel'); - $model->saveWithChildren([ - 'testmodel_text' => 'example1', - 'testmodel_flag' => 1, - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'example2', - 'testmodel_flag' => 1, - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'example3', - 'testmodel_flag' => 1, - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'example4', - 'testmodel_flag' => 1, - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'example5', - 'testmodel_flag' => 1, - ]); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - - \codename\core\app::getRequest()->setData($crudInstance::CRUD_FILTER_IDENTIFIER, [ - 'testmodel_flag' => 1, - ]); - - $resultView = $crudInstance->listview(); - $this->assertEmpty($resultView); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertEquals([ - 'testmodel_text', - 'testmodel_testmodeljoin_id', - 'testmodel_id', - ], $responseData['visibleFields']); - - $this->assertCount(2, $responseData['rows']); - $this->assertEquals('example3', $responseData['rows'][0]['testmodel_text']); - $this->assertEquals('example4', $responseData['rows'][1]['testmodel_text']); - - $this->assertEquals([ - 'crud_pagination_seek_enabled' => false, - 'crud_pagination_count' => 5, - 'crud_pagination_page' => 2, - 'crud_pagination_pages' => 3.0, - 'crud_pagination_limit' => 2, - ], [ - 'crud_pagination_seek_enabled' => $responseData['crud_pagination_seek_enabled'], - 'crud_pagination_count' => $responseData['crud_pagination_count'], - 'crud_pagination_page' => $responseData['crud_pagination_page'], - 'crud_pagination_pages' => $responseData['crud_pagination_pages'], - 'crud_pagination_limit' => $responseData['crud_pagination_limit'], - ]); - } - - /** - * [testCrudListViewFilter description] - */ - public function testCrudListViewFilter(): void { - \codename\core\app::getRequest()->setData('crud_pagination_page_prev', 2); - \codename\core\app::getRequest()->setData('crud_pagination_limit', 2); - - // set demo data - $model = $this->getModel('testmodel'); - $model->saveWithChildren([ - 'testmodel_text' => 'example1', - 'testmodel_number_natural' => 1, - 'testmodel_flag' => 3, - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'example2', - 'testmodel_number_natural' => 1, - 'testmodel_flag' => 3, - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'example3', - 'testmodel_number_natural' => 1, - 'testmodel_flag' => 3, - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'example4', - 'testmodel_number_natural' => 1, - 'testmodel_flag' => 3, - ]); - $model->saveWithChildren([ - 'testmodel_text' => 'example5', - 'testmodel_number_natural' => 1, - 'testmodel_flag' => 3, - ]); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->setConfig('crudtest_testmodel_filter'); - - // added filters - $now = new \DateTime('now'); - \codename\core\app::getRequest()->setData($crudInstance::CRUD_FILTER_IDENTIFIER, [ - 'provide_filter_example' => true, - 'testmodel_flag' => [ 1, 2 ], - 'testmodel_text' => [ - 'example2', - 'example3', - 'example4', - 'example5', - ], - 'testmodel_number_natural' => 1, - 'search' => 'example%', - 'testmodel_created' => [ - $now->format('Y-m-d 00:00:00'), - $now->format('Y-m-d 23:59:59'), - ], - ]); - - $crudInstance->provideFilter('provide_filter_example', [ - 'datatype' => 'text', - ], function(\codename\core\ui\crud $crudInstance, $filterValue) { - $crudInstance->getMyModel()->addFilter('testmodel_text', 'example1', '!='); - }); - - $resultView = $crudInstance->listview(); - $this->assertEmpty($resultView); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertEquals([ - 'testmodel_text', - 'testmodel_testmodeljoin_id', - 'testmodel_id', - ], $responseData['visibleFields']); - - $this->assertCount(2, $responseData['rows']); - $this->assertEquals('example4', $responseData['rows'][0]['testmodel_text']); - $this->assertEquals('example5', $responseData['rows'][1]['testmodel_text']); - } + ]); + + $crudInstance->provideFilter('provide_filter_example', [ + 'datatype' => 'text', + ], function (crud $crudInstance, $filterValue) { + $crudInstance->getMyModel()->addFilter('testmodel_text', 'example1', '!='); + }); + + try { + $crudInstance->listview(); + } catch (Exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertEquals([ + 'testmodel_text', + 'testmodel_testmodeljoin_id', + 'testmodel_id', + ], $responseData['visibleFields']); + + static::assertCount(2, $responseData['rows']); + static::assertEquals('example4', $responseData['rows'][0]['testmodel_text']); + static::assertEquals('example5', $responseData['rows'][1]['testmodel_text']); + } + + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws \codename\core\exception + */ + protected function tearDown(): void + { + $this->getModel('testmodel') + ->addFilter('testmodel_id', 0, '>') + ->delete(); + + $this->getModel('testmodeljoin') + ->addFilter('testmodeljoin_id', 0, '>') + ->delete(); + + $this->getModel('testmodelforcejoin') + ->addFilter('testmodelforcejoin_id', 0, '>') + ->delete(); + + $this->getModel('testmodelcollection') + ->addFilter('testmodelcollection_id', 0, '>') + ->delete(); + } + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws ErrorException + * @throws Throwable + * @throws compileErrorException + * @throws coreErrorException + * @throws coreWarningException + * @throws parseException + * @throws recoverableErrorException + * @throws strictException + * @throws userErrorException + * @throws userWarningException + * @throws warningException + * @throws \codename\core\exception + */ + protected function setUp(): void + { + overrideableApp::resetRequest(); + overrideableApp::resetResponse(); + $app = static::createApp(); + + // Additional overrides to get a more complete app lifecycle + // and allow static global app::getModel() to work correctly + $app::__setApp('crudtest'); + $app::__setVendor('codename'); + $app::__setNamespace('\\codename\\core\\ui\\tests\\crud'); + $app::__setHomedir(__DIR__); + + $app::getAppstack(); + + // avoid re-init + if (static::$initialized) { + return; + } + + // TODO: rework via overrideableApp + $app::__injectClientInstance('auth', 'default', new dummyAuth()); + + static::$initialized = true; + + static::setEnvironmentConfig([ + 'test' => [ + 'database' => [ + // NOTE: by default, we do these tests using + // pure in-memory sqlite. + 'default' => [ + 'driver' => 'sqlite', + // 'database_file' => 'testmodel.sqlite', + 'database_file' => ':memory:', + ], + ], + 'auth' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + 'cache' => [ + 'default' => [ + 'driver' => 'memory', + ], + ], + 'filesystem' => [ + 'local' => [ + 'driver' => 'local', + ], + ], + 'translate' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + 'templateengine' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + 'log' => [ + 'default' => [ + 'driver' => 'system', + 'data' => [ + 'name' => 'dummy', + ], + ], + ], + ], + ]); + + static::createModel( + 'crudtest', + 'testmodel', + testmodel::$staticConfig, + function ($schema, $model, $config) { + return new testmodel([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodeljoin', + testmodeljoin::$staticConfig, + function ($schema, $model, $config) { + return new testmodeljoin([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelforcejoin', + testmodelforcejoin::$staticConfig, + function ($schema, $model, $config) { + return new testmodelforcejoin([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelcollection', + testmodelcollection::$staticConfig, + function ($schema, $model, $config) { + return new testmodelcollection([]); + } + ); + + static::architect('crudtest', 'codename', 'test'); + } } -class dummyAuth extends \codename\core\auth { - /** - * @inheritDoc - */ - public function authenticate(\codename\core\credential $credential): array - { - throw new \LogicException('Not implemented'); // TODO - } - - /** - * @inheritDoc - */ - public function createCredential(array $parameters) : \codename\core\credential - { - throw new \LogicException('Not implemented'); // TODO - } - - /** - * @inheritDoc - */ - public function makeHash(\codename\core\credential $credential): string - { - throw new \LogicException('Not implemented'); // TODO - } - - /** - * @inheritDoc - */ - public function isAuthenticated(): bool - { - return true; // ? - } - - /** - * @inheritDoc - */ - public function memberOf(string $groupName): bool - { - if($groupName == 'group_true') { - return true; +class dummyAuth extends auth +{ + /** + * {@inheritDoc} + */ + public function authenticate(credential $credential): array + { + throw new LogicException('Not implemented'); // TODO + } + + /** + * {@inheritDoc} + */ + public function createCredential(array $parameters): credential + { + throw new LogicException('Not implemented'); // TODO + } + + /** + * {@inheritDoc} + */ + public function makeHash(credential $credential): string + { + throw new LogicException('Not implemented'); // TODO + } + + /** + * {@inheritDoc} + */ + public function isAuthenticated(): bool + { + return true; // ? + } + + /** + * {@inheritDoc} + */ + public function memberOf(string $groupName): bool + { + if ($groupName == 'group_true') { + return true; + } + return false; } - return false; - } } diff --git a/tests/crud/crudShowTest.php b/tests/crud/crudShowTest.php index 2b5d17a..8f6866a 100644 --- a/tests/crud/crudShowTest.php +++ b/tests/crud/crudShowTest.php @@ -1,194 +1,242 @@ getModel('testmodel') - ->addFilter('testmodel_id', 0, '>') - ->delete(); - - $this->getModel('testmodeljoin') - ->addFilter('testmodeljoin_id', 0, '>') - ->delete(); - - $this->getModel('testmodelforcejoin') - ->addFilter('testmodelforcejoin_id', 0, '>') - ->delete(); - - $this->getModel('testmodelcollection') - ->addFilter('testmodelcollection_id', 0, '>') - ->delete(); - } - - /** - * @inheritDoc - */ - protected function setUp(): void - { - overrideableApp::resetRequest(); - overrideableApp::resetResponse(); - $app = static::createApp(); - - // Additional overrides to get a more complete app lifecycle - // and allow static global app::getModel() to work correctly - $app->__setApp('crudtest'); - $app->__setVendor('codename'); - $app->__setNamespace('\\codename\\core\\ui\\tests\\crud'); - $app->__setHomedir(__DIR__); - - $app->getAppstack(); - - // avoid re-init - if(static::$initialized) { - return; + /** + * [testCrudShowForm description] + * @throws ReflectionException + * @throws DateMalformedStringException + * @throws exception + */ + public function testCrudShowForm(): void + { + $model = $this->getModel('testmodel'); + + // insert an entry and pass on PKEY for further 'editing' + $model->save([ + 'testmodel_text' => 'XYZ', + ]); + $id = $model->lastInsertId(); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + $crudInstance->eventCrudFormInit->addEventHandler( + new eventHandler(function (form $form) { + $form->fields = array_filter($form->fields, function (field $item) { + return $item->getConfig()->get('field_type') !== 'submit'; // only allow non-submits + }); + return $form; + }) + ); + + $crudInstance->show($id); + + // renderer? + static::assertEquals('frontend/form/compact/form', app::getResponse()->getData('form')); + + $form = $crudInstance->getForm(); + static::assertInstanceOf(form::class, $form); + + static::assertEquals('exampleTag', $form->config['form_tag']); + + $fields = $form->getFields(); + static::assertCount(8, $fields); + + foreach ($fields as $field) { + if ( + $field->getConfig()->get('field_name') == $model->getPrimaryKey() || + $field->getConfig()->get('field_type') == 'hidden' + ) { + continue; + } + static::assertTrue($field->getConfig()->get('field_readonly'), print_r($field->getConfig()->get(), true)); + } } - static::$initialized = true; - - static::setEnvironmentConfig([ - 'test' => [ - 'database' => [ - // NOTE: by default, we do these tests using - // pure in-memory sqlite. - 'default' => [ - 'driver' => 'sqlite', - // 'database_file' => 'testmodel.sqlite', - 'database_file' => ':memory:', - ], - ], - 'cache' => [ - 'default' => [ - 'driver' => 'memory' - ] - ], - 'filesystem' =>[ - 'local' => [ - 'driver' => 'local', - ] - ], - 'translate' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - 'templateengine' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - 'log' => [ - 'default' => [ - 'driver' => 'system', - 'data' => [ - 'name' => 'dummy' - ] - ] - ], - ] - ]); - - static::createModel( - 'crudtest', 'testmodel', - \codename\core\ui\tests\crud\model\testmodel::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodel([]); - } - ); - - static::createModel( - 'crudtest', 'testmodeljoin', - \codename\core\ui\tests\crud\model\testmodeljoin::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodeljoin([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelforcejoin', - \codename\core\ui\tests\crud\model\testmodelforcejoin::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelforcejoin([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelcollection', - \codename\core\ui\tests\crud\model\testmodelcollection::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelcollection([]); - } - ); - - static::architect('crudtest', 'codename', 'test'); - } - - /** - * [testCrudShowForm description] - */ - public function testCrudShowForm(): void { - $model = $this->getModel('testmodel'); - - // insert an entry and pass on PKEY for further 'editing' - $model->save([ - 'testmodel_text' => 'XYZ' - ]); - $id = $model->lastInsertId(); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - - $crudInstance->eventCrudFormInit->addEventHandler(new \codename\core\eventHandler(function(\codename\core\ui\form $form) { - $form->fields = array_filter($form->fields, function(\codename\core\ui\field $item) { - return $item->getConfig()->get('field_type') !== 'submit'; // only allow non-submits - }); - return $form; - })); - - $crudInstance->show($id); - - // renderer? - $this->assertEquals('frontend/form/compact/form', \codename\core\app::getResponse()->getData('form')); - - $form = $crudInstance->getForm(); - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - $this->assertEquals('exampleTag', $form->config['form_tag']); - - $fields = $form->getFields(); - $this->assertCount(8, $fields); - - foreach($fields as $field) { - if( - $field->getConfig()->get('field_name') == $model->getPrimarykey() || - $field->getConfig()->get('field_type') == 'hidden' - ) { - continue; - } - $this->assertTrue($field->getConfig()->get('field_readonly'), print_r($field->getConfig()->get(), true)); + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws exception + */ + protected function tearDown(): void + { + $this->getModel('testmodel') + ->addFilter('testmodel_id', 0, '>') + ->delete(); + + $this->getModel('testmodeljoin') + ->addFilter('testmodeljoin_id', 0, '>') + ->delete(); + + $this->getModel('testmodelforcejoin') + ->addFilter('testmodelforcejoin_id', 0, '>') + ->delete(); + + $this->getModel('testmodelcollection') + ->addFilter('testmodelcollection_id', 0, '>') + ->delete(); } - } + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws ErrorException + * @throws Throwable + * @throws compileErrorException + * @throws coreErrorException + * @throws coreWarningException + * @throws parseException + * @throws recoverableErrorException + * @throws strictException + * @throws userErrorException + * @throws userWarningException + * @throws warningException + * @throws exception + */ + protected function setUp(): void + { + overrideableApp::resetRequest(); + overrideableApp::resetResponse(); + $app = static::createApp(); + + // Additional overrides to get a more complete app lifecycle + // and allow static global app::getModel() to work correctly + $app::__setApp('crudtest'); + $app::__setVendor('codename'); + $app::__setNamespace('\\codename\\core\\ui\\tests\\crud'); + $app::__setHomedir(__DIR__); + + $app::getAppstack(); + + // avoid re-init + if (static::$initialized) { + return; + } + + static::$initialized = true; + + static::setEnvironmentConfig([ + 'test' => [ + 'database' => [ + // NOTE: by default, we do these tests using + // pure in-memory sqlite. + 'default' => [ + 'driver' => 'sqlite', + // 'database_file' => 'testmodel.sqlite', + 'database_file' => ':memory:', + ], + ], + 'cache' => [ + 'default' => [ + 'driver' => 'memory', + ], + ], + 'filesystem' => [ + 'local' => [ + 'driver' => 'local', + ], + ], + 'translate' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + 'templateengine' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + 'log' => [ + 'default' => [ + 'driver' => 'system', + 'data' => [ + 'name' => 'dummy', + ], + ], + ], + ], + ]); + + static::createModel( + 'crudtest', + 'testmodel', + testmodel::$staticConfig, + function ($schema, $model, $config) { + return new testmodel([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodeljoin', + testmodeljoin::$staticConfig, + function ($schema, $model, $config) { + return new testmodeljoin([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelforcejoin', + testmodelforcejoin::$staticConfig, + function ($schema, $model, $config) { + return new testmodelforcejoin([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelcollection', + testmodelcollection::$staticConfig, + function ($schema, $model, $config) { + return new testmodelcollection([]); + } + ); + + static::architect('crudtest', 'codename', 'test'); + } } diff --git a/tests/crud/crudTest.php b/tests/crud/crudTest.php index 6c46230..b87d264 100644 --- a/tests/crud/crudTest.php +++ b/tests/crud/crudTest.php @@ -1,731 +1,924 @@ getModel('testmodel') - ->addFilter('testmodel_id', 0, '>') - ->delete(); - - $this->getModel('testmodeljoin') - ->addFilter('testmodeljoin_id', 0, '>') - ->delete(); - - $this->getModel('testmodelforcejoin') - ->addFilter('testmodelforcejoin_id', 0, '>') - ->delete(); - - $this->getModel('testmodelcollection') - ->addFilter('testmodelcollection_id', 0, '>') - ->delete(); - - $this->getModel('testmodelwrongflag') - ->addFilter('testmodelwrongflag_id', 0, '>') - ->delete(); - - $this->getModel('testmodelwrongforeign') - ->addFilter('testmodelwrongforeign_id', 0, '>') - ->delete(); - - $this->getModel('testmodelwrongforeignorder') - ->addFilter('testmodelwrongforeignorder_id', 0, '>') - ->delete(); - - $this->getModel('testmodelwrongforeignfilter') - ->addFilter('testmodelwrongforeignfilter_id', 0, '>') - ->delete(); - - $this->getModel('testmodelcollectionforeign') - ->addFilter('testmodelcollectionforeign_id', 0, '>') - ->delete(); - } - - /** - * @inheritDoc - */ - protected function setUp(): void - { - overrideableApp::resetRequest(); - overrideableApp::resetResponse(); - $app = static::createApp(); - - // Additional overrides to get a more complete app lifecycle - // and allow static global app::getModel() to work correctly - $app->__setApp('crudtest'); - $app->__setVendor('codename'); - $app->__setNamespace('\\codename\\core\\ui\\tests\\crud'); - $app->__setHomedir(__DIR__); - - $app->getAppstack(); - - // avoid re-init - if(static::$initialized) { - return; - } - - static::$initialized = true; - - static::setEnvironmentConfig([ - 'test' => [ - 'database' => [ - // NOTE: by default, we do these tests using - // pure in-memory sqlite. - 'default' => [ - 'driver' => 'sqlite', - // 'database_file' => 'testmodel.sqlite', - 'database_file' => ':memory:', + /** + * Tests basic crud init + * @coversNothing + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudInit(): void + { + $this->expectNotToPerformAssertions(); + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + // just test if it doesn't crash + $crudInstance->create(); + } + + /** + * [testCrudInitWrongChildrenConfig description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudInitWrongChildrenConfig(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_CRUD_CHILDREN_CONFIG_MODEL_CONFIG_CHILDREN_IS_NULL'); + + $model = $this->getModel('testmodel'); + new crud($model, null, 'crudtest_testmodel_wrong_children'); + } + + /** + * [testCrudStats description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudStats(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + app::getRequest()->setData($crudInstance::CRUD_FILTER_IDENTIFIER, false); + + try { + $crudInstance->stats(); + } catch (exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + static::assertEquals([ + 'crud_pagination_seek_enabled' => false, + 'crud_pagination_count' => 0, + 'crud_pagination_page' => 1, + 'crud_pagination_pages' => 1, + 'crud_pagination_limit' => 5, + ], [ + 'crud_pagination_seek_enabled' => $responseData['crud_pagination_seek_enabled'], + 'crud_pagination_count' => $responseData['crud_pagination_count'], + 'crud_pagination_page' => $responseData['crud_pagination_page'], + 'crud_pagination_pages' => $responseData['crud_pagination_pages'], + 'crud_pagination_limit' => $responseData['crud_pagination_limit'], + ]); + } + + /** + * [testCrudDisplaytypeStatic description] + */ + public function testCrudDisplaytypeStatic(): void + { + $types = [ + 'structure_address' => 'structure_address', + 'structure_text_telephone' => 'structure_text_telephone', + 'structure' => 'structure', + 'boolean' => 'yesno', + 'text_date' => 'date', + 'text_date_birthdate' => 'date', + 'text_timestamp' => 'timestamp', + 'text_datetime_relative' => 'relativetime', + 'input' => 'input', + 'example' => 'input', + ]; + foreach ($types as $daType => $diType) { + $result = crud::getDisplaytypeStatic($daType); + static::assertEquals($diType, $result); + } + } + + /** + * [testCrudGetData description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudGetDataNull(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + $result = $crudInstance->getData('example'); + static::assertNull($result); + } + + /** + * [testCrudSetRequestDataAndNormalizationData description] + * @throws ReflectionException + * @throws DateMalformedStringException + * @throws exception + */ + public function testCrudSetRequestDataAndNormalizationData(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model, [ + 'testmodel_text' => 'moep', + 'testmodel_testmodeljoin' => [ + 'testmodeljoin_text' => 'se', + ], + 'wrong_field' => 'hello', + ]); + $crudInstance->useFormNormalizationData(); + + $result = $crudInstance->getData(); + static::assertEquals([ + 'testmodel_text' => 'moep', + 'testmodel_testmodeljoin' => [ + 'testmodeljoin_text' => 'se', + ], + ], $result); + } + + /** + * [testCrudUseDataAndGetData description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudUseDataAndGetData(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->useData([ + 'testmodel_text' => 'moep', + 'testmodel_testmodeljoin' => [ + 'testmodeljoin_text' => 'se', + ], + ]); + $result = $crudInstance->getData(); + static::assertEquals([ + 'testmodel_text' => 'moep', + 'testmodel_testmodeljoin' => [ + 'testmodeljoin_text' => 'se', ], - ], - 'cache' => [ - 'default' => [ - 'driver' => 'memory' - ] - ], - 'filesystem' =>[ - 'local' => [ - 'driver' => 'local', - ] - ], - 'translate' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - 'templateengine' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - 'log' => [ - 'default' => [ - 'driver' => 'system', - 'data' => [ - 'name' => 'dummy' - ] - ] - ], - ] - ]); - - static::createModel( - 'crudtest', 'testmodel', - \codename\core\ui\tests\crud\model\testmodel::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodel([]); - } - ); - - static::createModel( - 'crudtest', 'testmodeljoin', - \codename\core\ui\tests\crud\model\testmodeljoin::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodeljoin([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelforcejoin', - \codename\core\ui\tests\crud\model\testmodelforcejoin::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelforcejoin([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelcollection', - \codename\core\ui\tests\crud\model\testmodelcollection::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelcollection([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelwrongflag', - \codename\core\ui\tests\crud\model\testmodelwrongflag::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelwrongflag([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelwrongforeign', - \codename\core\ui\tests\crud\model\testmodelwrongforeign::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelwrongforeign([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelwrongforeignorder', - \codename\core\ui\tests\crud\model\testmodelwrongforeignorder::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelwrongforeignorder([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelwrongforeignfilter', - \codename\core\ui\tests\crud\model\testmodelwrongforeignfilter::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelwrongforeignfilter([]); - } - ); - - static::createModel( - 'crudtest', 'testmodelcollectionforeign', - \codename\core\ui\tests\crud\model\testmodelcollectionforeign::$staticConfig, - function($schema, $model, $config) { - return new \codename\core\ui\tests\crud\model\testmodelcollectionforeign([]); - } - ); - - static::architect('crudtest', 'codename', 'test'); - } - - /** - * Tests basic crud init - * @coversNothing - */ - public function testCrudInit(): void { - - $this->expectNotToPerformAssertions(); - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - - // just test, if it doesn't crash - $crudInstance->create(); - } - - /** - * [testCrudInitWrongChildrenConfig description] - */ - public function testCrudInitWrongChildrenConfig(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_CRUD_CHILDREN_CONFIG_MODEL_CONFIG_CHILDREN_IS_NULL'); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model, null, 'crudtest_testmodel_wrong_children'); - } - - /** - * [testCrudStats description] - */ - public function testCrudStats(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - \codename\core\app::getRequest()->setData($crudInstance::CRUD_FILTER_IDENTIFIER, false); - - $stats = $crudInstance->stats(); - $this->assertEmpty($stats); - - $responseData = overrideableApp::getResponse()->getData(); - $this->assertEquals([ - 'crud_pagination_seek_enabled' => false, - 'crud_pagination_count' => 0, - 'crud_pagination_page' => 1, - 'crud_pagination_pages' => 1, - 'crud_pagination_limit' => 5, - ], [ - 'crud_pagination_seek_enabled' => $responseData['crud_pagination_seek_enabled'], - 'crud_pagination_count' => $responseData['crud_pagination_count'], - 'crud_pagination_page' => $responseData['crud_pagination_page'], - 'crud_pagination_pages' => $responseData['crud_pagination_pages'], - 'crud_pagination_limit' => $responseData['crud_pagination_limit'], - ]); - } - - /** - * [testCrudDisplaytypeStatic description] - */ - public function testCrudDisplaytypeStatic(): void { - $types = [ - 'structure_address' => 'structure_address', - 'structure_text_telephone' => 'structure_text_telephone', - 'structure' => 'structure', - 'boolean' => 'yesno', - 'text_date' => 'date', - 'text_date_birthdate' => 'date', - 'text_timestamp' => 'timestamp', - 'text_datetime_relative' => 'relativetime', - 'input' => 'input', - 'example' => 'input', - ]; - foreach($types as $daType => $diType) { - $result = \codename\core\ui\crud::getDisplaytypeStatic($daType); - $this->assertEquals($diType, $result); - } - } - - /** - * [testCrudGetData description] - */ - public function testCrudGetDataNull(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - - $result = $crudInstance->getData('example'); - $this->assertNull($result); - - } - - /** - * [testCrudSetRequestDataAndNormalizationData description] - */ - public function testCrudSetRequestDataAndNormalizationData(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model, [ - 'testmodel_text' => 'moep', - 'testmodel_testmodeljoin' => [ - 'testmodeljoin_text' => 'se', - ], - 'wrong_field' => 'hello' - ]); - $crudInstance->useFormNormalizationData(); - - $result = $crudInstance->getData(); - $this->assertEquals([ - 'testmodel_text' => 'moep', - 'testmodel_testmodeljoin' => [ - 'testmodeljoin_text' => 'se', - ], - ], $result); - } - - /** - * [testCrudUseDataAndGetData description] - */ - public function testCrudUseDataAndGetData(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->useData([ - 'testmodel_text' => 'moep', - 'testmodel_testmodeljoin' => [ - 'testmodeljoin_text' => 'se', - ] - ]); - $result = $crudInstance->getData(); - $this->assertEquals([ - 'testmodel_text' => 'moep', - 'testmodel_testmodeljoin' => [ - 'testmodeljoin_text' => 'se', - ], - ], $result); - } - - /** - * [testCrudImport description] - */ - public function testCrudImport(): void { - $data = [ - [ 'testmodel_id' => 1, 'testmodel_text' => 'example1' ], - [ 'testmodel_id' => 2, 'testmodel_text' => 'example2' ], - [ 'testmodel_id' => 3, 'testmodel_text' => 'example3' ], - ]; - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $resultView = $crudInstance->import($data); - $this->assertEmpty($resultView); - - $responseData = overrideableApp::getResponse()->getData(); - - $dataClean = []; - foreach($data as $v) { - unset($v['testmodel_id']); - $dataClean[] = $v; - } - - $this->assertCount(3, $responseData['import_data']); - $this->assertEquals($dataClean, $responseData['import_data']); - - $res = $model->search()->getResult(); - $this->assertCount(3, $res); - - } - - /** - * [testCrudImportInvalid description] - */ - public function testCrudImportInvalid(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('CRUD_IMPORT_INVALID_DATASET'); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->import([ - [ 'testmodel_testmodeljoin_id' => 'example' ], - ]); - - } - - /** - * [testCrudExport description] - */ - public function testCrudExport(): void { - // set demo data - $model = $this->getModel('testmodel')->addModel($this->getModel('testmodeljoin')); - $model->saveWithChildren([ - 'testmodel_text' => 'example', - ]); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $resultView = $crudInstance->export(true); - $this->assertEmpty($resultView); - - $responseData = overrideableApp::getResponse()->getData(); - - $this->assertCount(1, $responseData['rows']); - $this->assertNotEmpty($responseData['rows'][0]['testmodel_id']); - $this->assertNotEmpty($responseData['rows'][0]['testmodel_created']); - $this->assertEmpty($responseData['rows'][0]['testmodel_modified']); - $this->assertEmpty($responseData['rows'][0]['testmodel_testmodeljoin_id']); - $this->assertEquals('example', $responseData['rows'][0]['testmodel_text']); - $this->assertEmpty($responseData['rows'][0]['testmodel_unique_single']); - $this->assertEmpty($responseData['rows'][0]['testmodel_unique_multi1']); - $this->assertEmpty($responseData['rows'][0]['testmodel_unique_multi2']); - $this->assertNotEmpty($responseData['rows'][0]['testmodel_testmodeljoin']); - $this->assertEquals([ - 'testmodeljoin_id' => null, - 'testmodeljoin_created' => null, - 'testmodeljoin_modified' => null, - 'testmodeljoin_text' => null, - ], $responseData['rows'][0]['testmodel_testmodeljoin']); - - } - - /** - * [testCrudMakeFormWithWrongField description] - */ - public function testCrudMakeFieldForeignFieldNotFound(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_FIELDNOTFOUNDINMODEL'); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model, null, 'crudtest_testmodel_field_not_found'); - $crudInstance->makeFieldForeign($model, 'example'); - } - - /** - * [testCrudMakeFieldFieldNotFound description] - */ - public function testCrudMakeFieldFieldNotFound(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_FIELDNOTFOUNDINMODEL'); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model, null, 'crudtest_testmodel_field_not_found'); - $crudInstance->makeField('example'); - } - - /** - * [testCrudMakeFieldInvalidReferenceObject description] - */ - public function testCrudMakeFieldForeignInvalidReferenceObject(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_INVALIDREFERENCEOBJECT'); - - $model = $this->getModel('testmodelwrongforeign'); - $crudInstance = new \codename\core\ui\crud($model, null, 'crudtest_testmodel_field_not_found'); - $crudInstance->makeFieldForeign($model, 'testmodelwrongforeign_testmodeljoin_id'); - } - - /** - * [testCrudMakeFieldInvalidOrderObject description] - */ - public function testCrudMakeFieldForeignInvalidOrderObject(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_INVALIDORDEROBJECT'); - - $model = $this->getModel('testmodelwrongforeignorder'); - $crudInstance = new \codename\core\ui\crud($model, null, 'crudtest_testmodel_field_not_found'); - $crudInstance->makeFieldForeign($model, 'testmodelwrongforeignorder_testmodeljoin_id'); - } - - /** - * [testCrudMakeFieldInvalidFilterObject description] - */ - public function testCrudMakeFieldForeignInvalidFilterObject(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_INVALIDFILTEROBJECT'); - - $model = $this->getModel('testmodelwrongforeignfilter'); - $crudInstance = new \codename\core\ui\crud($model, null, 'crudtest_testmodel_field_not_found'); - $crudInstance->makeFieldForeign($model, 'testmodelwrongforeignfilter_testmodeljoin_id'); - } - - /** - * [testCrudMakeFieldForeign description] - */ - public function testCrudMakeFieldForeignForeign(): void { - $model = $this->getModel('testmodelcollectionforeign'); - $crudInstance = new \codename\core\ui\crud($model); - $field = $crudInstance->makeFieldForeign($model, 'testmodelcollectionforeign_testmodel_id'); - - $this->assertInstanceOf(\codename\core\ui\field::class, $field); - } - - /** - * [testCrudMakeFieldInvalidReferenceObject description] - */ - public function testCrudMakeFieldInvalidReferenceObject(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_INVALIDREFERENCEOBJECT'); - - $model = $this->getModel('testmodelwrongforeign'); - $crudInstance = new \codename\core\ui\crud($model, null, 'crudtest_testmodel_field_not_found'); - $crudInstance->makeField('testmodelwrongforeign_testmodeljoin_id'); - } - - /** - * [testCrudMakeFieldInvalidOrderObject description] - */ - public function testCrudMakeFieldInvalidOrderObject(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_INVALIDORDEROBJECT'); - - $model = $this->getModel('testmodelwrongforeignorder'); - $crudInstance = new \codename\core\ui\crud($model, null, 'crudtest_testmodel_field_not_found'); - $crudInstance->makeField('testmodelwrongforeignorder_testmodeljoin_id'); - } - - /** - * [testCrudMakeFieldInvalidFilterObject description] - */ - public function testCrudMakeFieldInvalidFilterObject(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_INVALIDFILTEROBJECT'); - - $model = $this->getModel('testmodelwrongforeignfilter'); - $crudInstance = new \codename\core\ui\crud($model, null, 'crudtest_testmodel_field_not_found'); - $crudInstance->makeField('testmodelwrongforeignfilter_testmodeljoin_id'); - } - - /** - * [testCrudMakeFieldForeign description] - */ - public function testCrudMakeFieldForeign(): void { - $model = $this->getModel('testmodelcollectionforeign'); - $crudInstance = new \codename\core\ui\crud($model); - $field = $crudInstance->makeField('testmodelcollectionforeign_testmodel_id'); - - $this->assertInstanceOf(\codename\core\ui\field::class, $field); - } - - /** - * [testCrudMakeFormWithWrongField description] - */ - public function testCrudMakeFormWithWrongField(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_MAKEFORM_FIELDNOTFOUNDINMODEL'); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model, null, 'crudtest_testmodel_field_not_found'); - $form = $crudInstance->makeForm(null, false); - } - - /** - * [testCrudMakeFormWithWrongField description] - */ - public function testCrudMakeFormWithWrongFlag(): void { - $model = $this->getModel('testmodelwrongflag'); - $crudInstance = new \codename\core\ui\crud($model); - $form = $crudInstance->makeForm(null, false); - - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - $fields = $form->getFields(); - $this->assertCount(2, $fields); - } - - /** - * [testCrudUseFormWithFields description] - */ - public function testCrudUseFormWithFields(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->setConfigCache(true); - $crudInstance->useForm('testmodel'); - $crudInstance->useForm('testmodel'); // check for cache - $crudInstance->outputFormConfig = true; - $crudInstance->onFormfieldCreated = function(\codename\core\ui\field &$field) { - $field->setProperty('example', 'example'); - }; - $form = $crudInstance->makeForm(null, false); - - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - $fields = $form->getFields(); - $this->assertCount(4, $fields); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[0]); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[1]); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[2]); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[3]); - - $this->assertEquals('example', $fields[0]->getProperty('example')); - $this->assertEquals('example', $fields[1]->getProperty('example')); - $this->assertEquals('example', $fields[2]->getProperty('example')); - $this->assertEquals('example', $fields[3]->getProperty('example')); - } - - /** - * [testCrudUseFormWithFieldsets description] - */ - public function testCrudUseFormWithFieldsets(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $crudInstance->useForm('testmodel_fieldsets'); - $crudInstance->outputFormConfig = true; - $form = $crudInstance->makeForm(null, false); - - $this->assertInstanceOf(\codename\core\ui\form::class, $form); - - $fieldsets = $form->getFieldsets(); - $this->assertCount(2, $fieldsets); - $this->assertInstanceOf(\codename\core\ui\fieldset::class, $fieldsets[0]); - $this->assertInstanceOf(\codename\core\ui\fieldset::class, $fieldsets[1]); - - $fields = $fieldsets[0]->getFields(); - $this->assertCount(2, $fields); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[0]); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[1]); - - $fields = $fieldsets[1]->getFields(); - // CHANGED 2021-10-27: flag fields in fieldsets have not been handled correctly (legacy type) - $this->assertCount(2, $fields); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[0]); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[1]); - } - - /** - * [testCrudAddActionTopValid description] - */ - public function testCrudAddActionTopValid(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $return = $crudInstance->addTopaction([ - 'name' => 'exampleTopName', - 'view' => 'exampleTopView', - 'context' => 'exampleTopContext', - 'icon' => 'exampleTopIcon', - 'btnClass' => 'exampleTopBtnClass', - ]); - - $this->assertEmpty($result); - $this->assertEquals([ - 'name' => 'exampleTopName', - 'view' => 'exampleTopView', - 'context' => 'exampleTopContext', - 'icon' => 'exampleTopIcon', - 'btnClass' => 'exampleTopBtnClass', - ], $crudInstance->getConfig()->get('action>top>exampleTopName')); - } - - /** - * [testCrudAddActionBulkValid description] - */ - public function testCrudAddActionBulkValid(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $return = $crudInstance->addBulkaction([ - 'name' => 'exampleBulkName', - 'view' => 'exampleBulkView', - 'context' => 'exampleBulkContext', - 'icon' => 'exampleBulkIcon', - 'btnClass' => 'exampleBulkBtnClass', - ]); - - $this->assertEmpty($result); - $this->assertEquals([ - 'name' => 'exampleBulkName', - 'view' => 'exampleBulkView', - 'context' => 'exampleBulkContext', - 'icon' => 'exampleBulkIcon', - 'btnClass' => 'exampleBulkBtnClass', - ], $crudInstance->getConfig()->get('action>bulk>exampleBulkName')); - } - - /** - * [testCrudAddActionElementValid description] - */ - public function testCrudAddActionElementValid(): void { - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $return = $crudInstance->addElementaction([ - 'name' => 'exampleElementName', - 'view' => 'exampleElementView', - 'context' => 'exampleElementContext', - 'icon' => 'exampleElementIcon', - 'btnClass' => 'exampleElementBtnClass', - ]); - - $this->assertEmpty($result); - $this->assertEquals([ - 'name' => 'exampleElementName', - 'view' => 'exampleElementView', - 'context' => 'exampleElementContext', - 'icon' => 'exampleElementIcon', - 'btnClass' => 'exampleElementBtnClass', - ], $crudInstance->getConfig()->get('action>element>exampleElementName')); - } - - /** - * [testCrudAddActionTopInvalid description] - */ - public function testCrudAddActionTopInvalid(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_ADDACTION_INVALIDACTIONOBJECT'); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $return = $crudInstance->addTopaction([]); - } - - /** - * [testCrudAddActionBulkInvalid description] - */ - public function testCrudAddActionBulkInvalid(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_ADDACTION_INVALIDACTIONOBJECT'); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $return = $crudInstance->addBulkaction([]); - } - - /** - * [testCrudAddActionElementInvalid description] - */ - public function testCrudAddActionElementInvalid(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_ADDACTION_INVALIDACTIONOBJECT'); - - $model = $this->getModel('testmodel'); - $crudInstance = new \codename\core\ui\crud($model); - $return = $crudInstance->addElementaction([]); - } + ], $result); + } + + /** + * [testCrudImport description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudImport(): void + { + $data = [ + ['testmodel_id' => 1, 'testmodel_text' => 'example1'], + ['testmodel_id' => 2, 'testmodel_text' => 'example2'], + ['testmodel_id' => 3, 'testmodel_text' => 'example3'], + ]; + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + try { + $crudInstance->import($data); + } catch (exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + $dataClean = []; + foreach ($data as $v) { + unset($v['testmodel_id']); + $dataClean[] = $v; + } + + static::assertCount(3, $responseData['import_data']); + static::assertEquals($dataClean, $responseData['import_data']); + + $res = $model->search()->getResult(); + static::assertCount(3, $res); + } + + /** + * [testCrudImportInvalid description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudImportInvalid(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('CRUD_IMPORT_INVALID_DATASET'); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->import([ + ['testmodel_testmodeljoin_id' => 'example'], + ]); + } + + /** + * [testCrudExport description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + * @throws noticeException + */ + public function testCrudExport(): void + { + // set demo data + $model = $this->getModel('testmodel')->addModel($this->getModel('testmodeljoin')); + if (!($model instanceof sql)) { + static::fail('setup fail'); + } + $model->saveWithChildren([ + 'testmodel_text' => 'example', + ]); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + try { + $crudInstance->export(true); + } catch (exception) { + static::fail(); + } + + $responseData = overrideableApp::getResponse()->getData(); + + static::assertCount(1, $responseData['rows']); + static::assertNotEmpty($responseData['rows'][0]['testmodel_id']); + static::assertNotEmpty($responseData['rows'][0]['testmodel_created']); + static::assertEmpty($responseData['rows'][0]['testmodel_modified']); + static::assertEmpty($responseData['rows'][0]['testmodel_testmodeljoin_id']); + static::assertEquals('example', $responseData['rows'][0]['testmodel_text']); + static::assertEmpty($responseData['rows'][0]['testmodel_unique_single']); + static::assertEmpty($responseData['rows'][0]['testmodel_unique_multi1']); + static::assertEmpty($responseData['rows'][0]['testmodel_unique_multi2']); + static::assertNotEmpty($responseData['rows'][0]['testmodel_testmodeljoin']); + static::assertEquals([ + 'testmodeljoin_id' => null, + 'testmodeljoin_created' => null, + 'testmodeljoin_modified' => null, + 'testmodeljoin_text' => null, + ], $responseData['rows'][0]['testmodel_testmodeljoin']); + } + + /** + * [testCrudMakeFormWithWrongField description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudMakeFieldForeignFieldNotFound(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_FIELDNOTFOUNDINMODEL'); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model, null, 'crudtest_testmodel_field_not_found'); + $crudInstance->makeFieldForeign($model, 'example'); + } + + /** + * [testCrudMakeFieldFieldNotFound description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudMakeFieldFieldNotFound(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_FIELDNOTFOUNDINMODEL'); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model, null, 'crudtest_testmodel_field_not_found'); + $crudInstance->makeField('example'); + } + + /** + * [testCrudMakeFieldInvalidReferenceObject description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudMakeFieldForeignInvalidReferenceObject(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_INVALIDREFERENCEOBJECT'); + + $model = $this->getModel('testmodelwrongforeign'); + $crudInstance = new crud($model, null, 'crudtest_testmodel_field_not_found'); + $crudInstance->makeFieldForeign($model, 'testmodelwrongforeign_testmodeljoin_id'); + } + + /** + * [testCrudMakeFieldInvalidOrderObject description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudMakeFieldForeignInvalidOrderObject(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_INVALIDORDEROBJECT'); + + $model = $this->getModel('testmodelwrongforeignorder'); + $crudInstance = new crud($model, null, 'crudtest_testmodel_field_not_found'); + $crudInstance->makeFieldForeign($model, 'testmodelwrongforeignorder_testmodeljoin_id'); + } + + /** + * [testCrudMakeFieldInvalidFilterObject description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudMakeFieldForeignInvalidFilterObject(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_INVALIDFILTEROBJECT'); + + $model = $this->getModel('testmodelwrongforeignfilter'); + $crudInstance = new crud($model, null, 'crudtest_testmodel_field_not_found'); + $crudInstance->makeFieldForeign($model, 'testmodelwrongforeignfilter_testmodeljoin_id'); + } + /** + * [testCrudMakeFieldForeign description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudMakeFieldForeignForeign(): void + { + $model = $this->getModel('testmodelcollectionforeign'); + $crudInstance = new crud($model); + $field = $crudInstance->makeFieldForeign($model, 'testmodelcollectionforeign_testmodel_id'); + + static::assertInstanceOf(field::class, $field); + } + + /** + * [testCrudMakeFieldInvalidReferenceObject description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudMakeFieldInvalidReferenceObject(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_INVALIDREFERENCEOBJECT'); + + $model = $this->getModel('testmodelwrongforeign'); + $crudInstance = new crud($model, null, 'crudtest_testmodel_field_not_found'); + $crudInstance->makeField('testmodelwrongforeign_testmodeljoin_id'); + } + + /** + * [testCrudMakeFieldInvalidOrderObject description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudMakeFieldInvalidOrderObject(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_INVALIDORDEROBJECT'); + + $model = $this->getModel('testmodelwrongforeignorder'); + $crudInstance = new crud($model, null, 'crudtest_testmodel_field_not_found'); + $crudInstance->makeField('testmodelwrongforeignorder_testmodeljoin_id'); + } + + /** + * [testCrudMakeFieldInvalidFilterObject description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudMakeFieldInvalidFilterObject(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_MAKEFIELD_INVALIDFILTEROBJECT'); + + $model = $this->getModel('testmodelwrongforeignfilter'); + $crudInstance = new crud($model, null, 'crudtest_testmodel_field_not_found'); + $crudInstance->makeField('testmodelwrongforeignfilter_testmodeljoin_id'); + } + + /** + * [testCrudMakeFieldForeign description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudMakeFieldForeign(): void + { + $model = $this->getModel('testmodelcollectionforeign'); + $crudInstance = new crud($model); + $field = $crudInstance->makeField('testmodelcollectionforeign_testmodel_id'); + + static::assertInstanceOf(field::class, $field); + } + + /** + * [testCrudMakeFormWithWrongField description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudMakeFormWithWrongField(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_MAKEFORM_FIELDNOTFOUNDINMODEL'); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model, null, 'crudtest_testmodel_field_not_found'); + $crudInstance->makeForm(null, false); + } + + /** + * [testCrudMakeFormWithWrongField description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudMakeFormWithWrongFlag(): void + { + $model = $this->getModel('testmodelwrongflag'); + $crudInstance = new crud($model); + $form = $crudInstance->makeForm(null, false); + + static::assertInstanceOf(form::class, $form); + + $fields = $form->getFields(); + static::assertCount(2, $fields); + } + + /** + * [testCrudUseFormWithFields description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudUseFormWithFields(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->setConfigCache(true); + $crudInstance->useForm('testmodel'); + $crudInstance->useForm('testmodel'); // check for cache + $crudInstance->outputFormConfig = true; + $crudInstance->onFormfieldCreated = function (field $field) { + $field->setProperty('example', 'example'); + }; + $form = $crudInstance->makeForm(null, false); + + static::assertInstanceOf(form::class, $form); + + $fields = $form->getFields(); + static::assertCount(4, $fields); + static::assertInstanceOf(field::class, $fields[0]); + static::assertInstanceOf(field::class, $fields[1]); + static::assertInstanceOf(field::class, $fields[2]); + static::assertInstanceOf(field::class, $fields[3]); + + static::assertEquals('example', $fields[0]->getProperty('example')); + static::assertEquals('example', $fields[1]->getProperty('example')); + static::assertEquals('example', $fields[2]->getProperty('example')); + static::assertEquals('example', $fields[3]->getProperty('example')); + } + + /** + * [testCrudUseFormWithFieldsets description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudUseFormWithFieldsets(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->useForm('testmodel_fieldsets'); + $crudInstance->outputFormConfig = true; + $form = $crudInstance->makeForm(null, false); + + static::assertInstanceOf(form::class, $form); + + $fieldsets = $form->getFieldsets(); + static::assertCount(2, $fieldsets); + static::assertInstanceOf(fieldset::class, $fieldsets[0]); + static::assertInstanceOf(fieldset::class, $fieldsets[1]); + + $fields = $fieldsets[0]->getFields(); + static::assertCount(2, $fields); + static::assertInstanceOf(field::class, $fields[0]); + static::assertInstanceOf(field::class, $fields[1]); + + $fields = $fieldsets[1]->getFields(); + // CHANGED 2021-10-27: flag fields in fieldsets have not been handled correctly (legacy type) + static::assertCount(2, $fields); + static::assertInstanceOf(field::class, $fields[0]); + static::assertInstanceOf(field::class, $fields[1]); + } + + /** + * [testCrudAddActionTopValid description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudAddActionTopValid(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + try { + $crudInstance->addTopaction([ + 'name' => 'exampleTopName', + 'view' => 'exampleTopView', + 'context' => 'exampleTopContext', + 'icon' => 'exampleTopIcon', + 'btnClass' => 'exampleTopBtnClass', + ]); + } catch (exception) { + static::fail(); + } + + static::assertEquals([ + 'name' => 'exampleTopName', + 'view' => 'exampleTopView', + 'context' => 'exampleTopContext', + 'icon' => 'exampleTopIcon', + 'btnClass' => 'exampleTopBtnClass', + ], $crudInstance->getConfig()->get('action>top>exampleTopName')); + } + + /** + * [testCrudAddActionBulkValid description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudAddActionBulkValid(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + try { + $crudInstance->addBulkaction([ + 'name' => 'exampleBulkName', + 'view' => 'exampleBulkView', + 'context' => 'exampleBulkContext', + 'icon' => 'exampleBulkIcon', + 'btnClass' => 'exampleBulkBtnClass', + ]); + } catch (exception) { + static::fail(); + } + + static::assertEquals([ + 'name' => 'exampleBulkName', + 'view' => 'exampleBulkView', + 'context' => 'exampleBulkContext', + 'icon' => 'exampleBulkIcon', + 'btnClass' => 'exampleBulkBtnClass', + ], $crudInstance->getConfig()->get('action>bulk>exampleBulkName')); + } + + /** + * [testCrudAddActionElementValid description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudAddActionElementValid(): void + { + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + + try { + $crudInstance->addElementaction([ + 'name' => 'exampleElementName', + 'view' => 'exampleElementView', + 'context' => 'exampleElementContext', + 'icon' => 'exampleElementIcon', + 'btnClass' => 'exampleElementBtnClass', + ]); + } catch (exception) { + static::fail(); + } + + static::assertEquals([ + 'name' => 'exampleElementName', + 'view' => 'exampleElementView', + 'context' => 'exampleElementContext', + 'icon' => 'exampleElementIcon', + 'btnClass' => 'exampleElementBtnClass', + ], $crudInstance->getConfig()->get('action>element>exampleElementName')); + } + + /** + * [testCrudAddActionTopInvalid description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudAddActionTopInvalid(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_ADDACTION_INVALIDACTIONOBJECT'); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->addTopaction([]); + } + + /** + * [testCrudAddActionBulkInvalid description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudAddActionBulkInvalid(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_ADDACTION_INVALIDACTIONOBJECT'); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->addBulkaction([]); + } + + /** + * [testCrudAddActionElementInvalid description] + * @throws DateMalformedStringException + * @throws ReflectionException + * @throws exception + */ + public function testCrudAddActionElementInvalid(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_ADDACTION_INVALIDACTIONOBJECT'); + + $model = $this->getModel('testmodel'); + $crudInstance = new crud($model); + $crudInstance->addElementaction([]); + } + + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws exception + */ + protected function tearDown(): void + { + $this->getModel('testmodel') + ->addFilter('testmodel_id', 0, '>') + ->delete(); + + $this->getModel('testmodeljoin') + ->addFilter('testmodeljoin_id', 0, '>') + ->delete(); + + $this->getModel('testmodelforcejoin') + ->addFilter('testmodelforcejoin_id', 0, '>') + ->delete(); + + $this->getModel('testmodelcollection') + ->addFilter('testmodelcollection_id', 0, '>') + ->delete(); + + $this->getModel('testmodelwrongflag') + ->addFilter('testmodelwrongflag_id', 0, '>') + ->delete(); + + $this->getModel('testmodelwrongforeign') + ->addFilter('testmodelwrongforeign_id', 0, '>') + ->delete(); + + $this->getModel('testmodelwrongforeignorder') + ->addFilter('testmodelwrongforeignorder_id', 0, '>') + ->delete(); + + $this->getModel('testmodelwrongforeignfilter') + ->addFilter('testmodelwrongforeignfilter_id', 0, '>') + ->delete(); + + $this->getModel('testmodelcollectionforeign') + ->addFilter('testmodelcollectionforeign_id', 0, '>') + ->delete(); + } + + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws ErrorException + * @throws Throwable + * @throws compileErrorException + * @throws coreErrorException + * @throws coreWarningException + * @throws parseException + * @throws recoverableErrorException + * @throws strictException + * @throws userErrorException + * @throws userWarningException + * @throws warningException + * @throws exception + */ + protected function setUp(): void + { + overrideableApp::resetRequest(); + overrideableApp::resetResponse(); + $app = static::createApp(); + + // Additional overrides to get a more complete app lifecycle + // and allow static global app::getModel() to work correctly + $app::__setApp('crudtest'); + $app::__setVendor('codename'); + $app::__setNamespace('\\codename\\core\\ui\\tests\\crud'); + $app::__setHomedir(__DIR__); + + $app::getAppstack(); + + // avoid re-init + if (static::$initialized) { + return; + } + + static::$initialized = true; + + static::setEnvironmentConfig([ + 'test' => [ + 'database' => [ + // NOTE: by default, we do these tests using + // pure in-memory sqlite. + 'default' => [ + 'driver' => 'sqlite', + // 'database_file' => 'testmodel.sqlite', + 'database_file' => ':memory:', + ], + ], + 'cache' => [ + 'default' => [ + 'driver' => 'memory', + ], + ], + 'filesystem' => [ + 'local' => [ + 'driver' => 'local', + ], + ], + 'translate' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + 'templateengine' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + 'log' => [ + 'default' => [ + 'driver' => 'system', + 'data' => [ + 'name' => 'dummy', + ], + ], + ], + ], + ]); + + static::createModel( + 'crudtest', + 'testmodel', + testmodel::$staticConfig, + function ($schema, $model, $config) { + return new testmodel([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodeljoin', + testmodeljoin::$staticConfig, + function ($schema, $model, $config) { + return new testmodeljoin([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelforcejoin', + testmodelforcejoin::$staticConfig, + function ($schema, $model, $config) { + return new testmodelforcejoin([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelcollection', + testmodelcollection::$staticConfig, + function ($schema, $model, $config) { + return new testmodelcollection([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelwrongflag', + testmodelwrongflag::$staticConfig, + function ($schema, $model, $config) { + return new testmodelwrongflag([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelwrongforeign', + testmodelwrongforeign::$staticConfig, + function ($schema, $model, $config) { + return new testmodelwrongforeign([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelwrongforeignorder', + testmodelwrongforeignorder::$staticConfig, + function ($schema, $model, $config) { + return new testmodelwrongforeignorder([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelwrongforeignfilter', + testmodelwrongforeignfilter::$staticConfig, + function ($schema, $model, $config) { + return new testmodelwrongforeignfilter([]); + } + ); + + static::createModel( + 'crudtest', + 'testmodelcollectionforeign', + testmodelcollectionforeign::$staticConfig, + function ($schema, $model, $config) { + return new testmodelcollectionforeign([]); + } + ); + + static::architect('crudtest', 'codename', 'test'); + } } diff --git a/tests/crud/model/testmodel.php b/tests/crud/model/testmodel.php index 27f6796..ec575a7 100644 --- a/tests/crud/model/testmodel.php +++ b/tests/crud/model/testmodel.php @@ -1,107 +1,111 @@ [ - 'testmodel_id', - 'testmodel_created', - 'testmodel_modified', - 'testmodel_testmodeljoin_id', - 'testmodel_testmodeljoin', - 'testmodel_testmodelcollection', - 'testmodel_text', - 'testmodel_number_natural', - 'testmodel_boolean', - 'testmodel_flag', - 'testmodel_unique_single', - 'testmodel_unique_multi1', - 'testmodel_unique_multi2', - ], - 'flag' => [ - 'example1' => 1, - 'example2' => 2, - 'example4' => 4, - 'example8' => 8, - ], - 'primary' => [ - 'testmodel_id' - ], - 'unique' => [ - 'testmodel_unique_single', - [ 'testmodel_unique_multi1', 'testmodel_unique_multi2' ], - ], - 'children' => [ - 'testmodel_testmodeljoin' => [ - 'type' => 'foreign', - 'field' => 'testmodel_testmodeljoin_id', +class testmodel extends sqlModel +{ + /** + * static configuration + * for usage in unit tests + * @var array + */ + public static array $staticConfig = [ + 'field' => [ + 'testmodel_id', + 'testmodel_created', + 'testmodel_modified', + 'testmodel_testmodeljoin_id', + 'testmodel_testmodeljoin', + 'testmodel_testmodelcollection', + 'testmodel_text', + 'testmodel_number_natural', + 'testmodel_boolean', + 'testmodel_flag', + 'testmodel_unique_single', + 'testmodel_unique_multi1', + 'testmodel_unique_multi2', + ], + 'flag' => [ + 'example1' => 1, + 'example2' => 2, + 'example4' => 4, + 'example8' => 8, + ], + 'primary' => [ + 'testmodel_id', ], - 'testmodel_testmodelcollection' => [ - 'type' => 'collection', - 'field' => 'testmodel_testmodeljoin_id', - ] - ], - 'collection' => [ - 'testmodel_testmodelcollection' => [ - 'schema' => 'crudtest', - 'model' => 'testmodelcollection', - 'key' => 'testmodelcollection_testmodel_id', - ] - ], - 'foreign' => [ - 'testmodel_testmodeljoin_id' => [ - 'schema' => 'crudtest', - 'model' => 'testmodeljoin', - 'key' => 'testmodeljoin_id', - 'display' => '{$element["testmodeljoin_text"]}' + 'unique' => [ + 'testmodel_unique_single', + ['testmodel_unique_multi1', 'testmodel_unique_multi2'], ], - ], - 'required' => [ - 'testmodel_text' - ], - 'options' => [ - 'testmodel_unique_single' => [ - 'length' => 16 + 'children' => [ + 'testmodel_testmodeljoin' => [ + 'type' => 'foreign', + 'field' => 'testmodel_testmodeljoin_id', + ], + 'testmodel_testmodelcollection' => [ + 'type' => 'collection', + 'field' => 'testmodel_testmodeljoin_id', + ], ], - 'testmodel_unique_multi1' => [ - 'length' => 16 + 'collection' => [ + 'testmodel_testmodelcollection' => [ + 'schema' => 'crudtest', + 'model' => 'testmodelcollection', + 'key' => 'testmodelcollection_testmodel_id', + ], ], - 'testmodel_unique_multi2' => [ - 'length' => 16 + 'foreign' => [ + 'testmodel_testmodeljoin_id' => [ + 'schema' => 'crudtest', + 'model' => 'testmodeljoin', + 'key' => 'testmodeljoin_id', + 'display' => '{$element["testmodeljoin_text"]}', + ], ], - ], - 'datatype' => [ - 'testmodel_id' => 'number_natural', - 'testmodel_created' => 'text_timestamp', - 'testmodel_modified' => 'text_timestamp', - 'testmodel_testmodeljoin_id' => 'number_natural', - 'testmodel_testmodeljoin' => 'virtual', - 'testmodel_testmodelcollection' => 'virtual', - 'testmodel_text' => 'text', - 'testmodel_number_natural' => 'number_natural', - 'testmodel_boolean' => 'boolean', - 'testmodel_flag' => 'number_natural', - 'testmodel_unique_single' => 'text', - 'testmodel_unique_multi1' => 'text', - 'testmodel_unique_multi2' => 'text', - ], - 'connection' => 'default' - ]; + 'required' => [ + 'testmodel_text', + ], + 'options' => [ + 'testmodel_unique_single' => [ + 'length' => 16, + ], + 'testmodel_unique_multi1' => [ + 'length' => 16, + ], + 'testmodel_unique_multi2' => [ + 'length' => 16, + ], + ], + 'datatype' => [ + 'testmodel_id' => 'number_natural', + 'testmodel_created' => 'text_timestamp', + 'testmodel_modified' => 'text_timestamp', + 'testmodel_testmodeljoin_id' => 'number_natural', + 'testmodel_testmodeljoin' => 'virtual', + 'testmodel_testmodelcollection' => 'virtual', + 'testmodel_text' => 'text', + 'testmodel_number_natural' => 'number_natural', + 'testmodel_boolean' => 'boolean', + 'testmodel_flag' => 'number_natural', + 'testmodel_unique_single' => 'text', + 'testmodel_unique_multi1' => 'text', + 'testmodel_unique_multi2' => 'text', + ], + 'connection' => 'default', + ]; + + /** + * {@inheritDoc} + */ + public function __construct(array $modeldata = []) + { + parent::__construct('crudtest', 'testmodel', static::$staticConfig); + } } diff --git a/tests/crud/model/testmodelcollection.php b/tests/crud/model/testmodelcollection.php index 1d74c86..e309538 100644 --- a/tests/crud/model/testmodelcollection.php +++ b/tests/crud/model/testmodelcollection.php @@ -1,50 +1,54 @@ [ - 'testmodelcollection_id', - 'testmodelcollection_created', - 'testmodelcollection_modified', - 'testmodelcollection_testmodel_id', - 'testmodelcollection_text', - ], - 'primary' => [ - 'testmodelcollection_id' - ], - 'foreign' => [ - 'testmodelcollection_testmodel_id' => [ - 'schema' => 'crudtest', - 'model' => 'testmodel', - 'key' => 'testmodel_id', - 'display' => '{$element["testmodel_text"]}' +class testmodelcollection extends sqlModel +{ + /** + * static configuration + * for usage in unit tests + * @var array + */ + public static array $staticConfig = [ + 'field' => [ + 'testmodelcollection_id', + 'testmodelcollection_created', + 'testmodelcollection_modified', + 'testmodelcollection_testmodel_id', + 'testmodelcollection_text', + ], + 'primary' => [ + 'testmodelcollection_id', ], - ], - 'datatype' => [ - 'testmodelcollection_id' => 'number_natural', - 'testmodelcollection_created' => 'text_timestamp', - 'testmodelcollection_modified' => 'text_timestamp', - 'testmodelcollection_testmodel_id' => 'number_natural', - 'testmodelcollection_text' => 'text', - ], - 'connection' => 'default' - ]; + 'foreign' => [ + 'testmodelcollection_testmodel_id' => [ + 'schema' => 'crudtest', + 'model' => 'testmodel', + 'key' => 'testmodel_id', + 'display' => '{$element["testmodel_text"]}', + ], + ], + 'datatype' => [ + 'testmodelcollection_id' => 'number_natural', + 'testmodelcollection_created' => 'text_timestamp', + 'testmodelcollection_modified' => 'text_timestamp', + 'testmodelcollection_testmodel_id' => 'number_natural', + 'testmodelcollection_text' => 'text', + ], + 'connection' => 'default', + ]; + + /** + * {@inheritDoc} + */ + public function __construct(array $modeldata = []) + { + parent::__construct('crudtest', 'testmodelcollection', static::$staticConfig); + } } diff --git a/tests/crud/model/testmodelcollectionforeign.php b/tests/crud/model/testmodelcollectionforeign.php index 7ba8f88..cc9f5b5 100644 --- a/tests/crud/model/testmodelcollectionforeign.php +++ b/tests/crud/model/testmodelcollectionforeign.php @@ -1,73 +1,77 @@ [ - 'testmodelcollectionforeign_id', - 'testmodelcollectionforeign_created', - 'testmodelcollectionforeign_modified', - 'testmodelcollectionforeign_testmodel_id', - 'testmodelcollectionforeign_text', - ], - 'primary' => [ - 'testmodelcollectionforeign_id' - ], - 'foreign' => [ - 'testmodelcollectionforeign_testmodel_id' => [ - 'schema' => 'crudtest', - 'model' => 'testmodel', - 'key' => 'testmodel_id', - 'display' => '{$element["testmodel_text"]}', - 'order' => [ - [ - 'field' => 'testmodel_id', - 'direction' => 'ASC', - ], - ], - 'filter' => [ - [ - 'field' => 'testmodel_id', - 'operator' => '!=', - 'value' => null, - ], - [ - 'field' => 'testmodel_flag', - 'operator' => '=', - 'value' => 'example1', +class testmodelcollectionforeign extends sqlModel +{ + /** + * static configuration + * for usage in unit tests + * @var array + */ + public static array $staticConfig = [ + 'field' => [ + 'testmodelcollectionforeign_id', + 'testmodelcollectionforeign_created', + 'testmodelcollectionforeign_modified', + 'testmodelcollectionforeign_testmodel_id', + 'testmodelcollectionforeign_text', + ], + 'primary' => [ + 'testmodelcollectionforeign_id', + ], + 'foreign' => [ + 'testmodelcollectionforeign_testmodel_id' => [ + 'schema' => 'crudtest', + 'model' => 'testmodel', + 'key' => 'testmodel_id', + 'display' => '{$element["testmodel_text"]}', + 'order' => [ + [ + 'field' => 'testmodel_id', + 'direction' => 'ASC', + ], ], - [ - 'field' => 'testmodel_flag', - 'operator' => '!=', - 'value' => 'example2', + 'filter' => [ + [ + 'field' => 'testmodel_id', + 'operator' => '!=', + 'value' => null, + ], + [ + 'field' => 'testmodel_flag', + 'operator' => '=', + 'value' => 'example1', + ], + [ + 'field' => 'testmodel_flag', + 'operator' => '!=', + 'value' => 'example2', + ], ], ], ], - ], - 'datatype' => [ - 'testmodelcollectionforeign_id' => 'number_natural', - 'testmodelcollectionforeign_created' => 'text_timestamp', - 'testmodelcollectionforeign_modified' => 'text_timestamp', - 'testmodelcollectionforeign_testmodel_id' => 'number_natural', - 'testmodelcollectionforeign_text' => 'text', - ], - 'connection' => 'default' - ]; + 'datatype' => [ + 'testmodelcollectionforeign_id' => 'number_natural', + 'testmodelcollectionforeign_created' => 'text_timestamp', + 'testmodelcollectionforeign_modified' => 'text_timestamp', + 'testmodelcollectionforeign_testmodel_id' => 'number_natural', + 'testmodelcollectionforeign_text' => 'text', + ], + 'connection' => 'default', + ]; + + /** + * {@inheritDoc} + */ + public function __construct(array $modeldata = []) + { + parent::__construct('crudtest', 'testmodelcollectionforeign', static::$staticConfig); + } } diff --git a/tests/crud/model/testmodelforcejoin.php b/tests/crud/model/testmodelforcejoin.php index e23cb9f..f711787 100644 --- a/tests/crud/model/testmodelforcejoin.php +++ b/tests/crud/model/testmodelforcejoin.php @@ -1,90 +1,94 @@ [ - 'testmodelforcejoin_id', - 'testmodelforcejoin_created', - 'testmodelforcejoin_modified', - 'testmodelforcejoin_testmodeljoin_id', - 'testmodelforcejoin_testmodeljoin', - 'testmodelforcejoin_text', - 'testmodelforcejoin_number_natural', - 'testmodelforcejoin_flag', - 'testmodelforcejoin_unique_single', - 'testmodelforcejoin_unique_multi1', - 'testmodelforcejoin_unique_multi2', - ], - 'flag' => [ - 'example1' => 1, - 'example2' => 2, - 'example4' => 4, - 'example8' => 8, - ], - 'primary' => [ - 'testmodelforcejoin_id' - ], - 'unique' => [ - 'testmodelforcejoin_unique_single', - [ 'testmodelforcejoin_unique_multi1', 'testmodelforcejoin_unique_multi2' ], - ], - 'children' => [ - 'testmodelforcejoin_testmodeljoin' => [ - 'type' => 'foreign', - 'field' => 'testmodelforcejoin_testmodeljoin_id', - 'force_virtual_join' => true, - ] - ], - 'foreign' => [ - 'testmodelforcejoin_testmodeljoin_id' => [ - 'schema' => 'crudtest', - 'model' => 'testmodeljoin', - 'key' => 'testmodeljoin_id', - 'display' => '{$element["testmodeljoin_text"]}' +class testmodelforcejoin extends sqlModel +{ + /** + * static configuration + * for usage in unit tests + * @var array + */ + public static array $staticConfig = [ + 'field' => [ + 'testmodelforcejoin_id', + 'testmodelforcejoin_created', + 'testmodelforcejoin_modified', + 'testmodelforcejoin_testmodeljoin_id', + 'testmodelforcejoin_testmodeljoin', + 'testmodelforcejoin_text', + 'testmodelforcejoin_number_natural', + 'testmodelforcejoin_flag', + 'testmodelforcejoin_unique_single', + 'testmodelforcejoin_unique_multi1', + 'testmodelforcejoin_unique_multi2', + ], + 'flag' => [ + 'example1' => 1, + 'example2' => 2, + 'example4' => 4, + 'example8' => 8, ], - ], - 'options' => [ - 'testmodelforcejoin_unique_single' => [ - 'length' => 16 + 'primary' => [ + 'testmodelforcejoin_id', ], - 'testmodelforcejoin_unique_multi1' => [ - 'length' => 16 + 'unique' => [ + 'testmodelforcejoin_unique_single', + ['testmodelforcejoin_unique_multi1', 'testmodelforcejoin_unique_multi2'], ], - 'testmodelforcejoin_unique_multi2' => [ - 'length' => 16 + 'children' => [ + 'testmodelforcejoin_testmodeljoin' => [ + 'type' => 'foreign', + 'field' => 'testmodelforcejoin_testmodeljoin_id', + 'force_virtual_join' => true, + ], ], - ], - 'datatype' => [ - 'testmodelforcejoin_id' => 'number_natural', - 'testmodelforcejoin_created' => 'text_timestamp', - 'testmodelforcejoin_modified' => 'text_timestamp', - 'testmodelforcejoin_testmodeljoin_id' => 'number_natural', - 'testmodelforcejoin_testmodeljoin' => 'virtual', - 'testmodelforcejoin_text' => 'text', - 'testmodelforcejoin_number_natural' => 'number_natural', - 'testmodelforcejoin_flag' => 'number_natural', - 'testmodelforcejoin_unique_single' => 'text', - 'testmodelforcejoin_unique_multi1' => 'text', - 'testmodelforcejoin_unique_multi2' => 'text', - ], - 'connection' => 'default' - ]; + 'foreign' => [ + 'testmodelforcejoin_testmodeljoin_id' => [ + 'schema' => 'crudtest', + 'model' => 'testmodeljoin', + 'key' => 'testmodeljoin_id', + 'display' => '{$element["testmodeljoin_text"]}', + ], + ], + 'options' => [ + 'testmodelforcejoin_unique_single' => [ + 'length' => 16, + ], + 'testmodelforcejoin_unique_multi1' => [ + 'length' => 16, + ], + 'testmodelforcejoin_unique_multi2' => [ + 'length' => 16, + ], + ], + 'datatype' => [ + 'testmodelforcejoin_id' => 'number_natural', + 'testmodelforcejoin_created' => 'text_timestamp', + 'testmodelforcejoin_modified' => 'text_timestamp', + 'testmodelforcejoin_testmodeljoin_id' => 'number_natural', + 'testmodelforcejoin_testmodeljoin' => 'virtual', + 'testmodelforcejoin_text' => 'text', + 'testmodelforcejoin_number_natural' => 'number_natural', + 'testmodelforcejoin_flag' => 'number_natural', + 'testmodelforcejoin_unique_single' => 'text', + 'testmodelforcejoin_unique_multi1' => 'text', + 'testmodelforcejoin_unique_multi2' => 'text', + ], + 'connection' => 'default', + ]; + + /** + * {@inheritDoc} + */ + public function __construct(array $modeldata = []) + { + parent::__construct('crudtest', 'testmodelforcejoin', static::$staticConfig); + } } diff --git a/tests/crud/model/testmodeljoin.php b/tests/crud/model/testmodeljoin.php index 30fc980..b30b89c 100644 --- a/tests/crud/model/testmodeljoin.php +++ b/tests/crud/model/testmodeljoin.php @@ -1,40 +1,44 @@ [ + 'testmodeljoin_id', + 'testmodeljoin_created', + 'testmodeljoin_modified', + 'testmodeljoin_text', + ], + 'primary' => [ + 'testmodeljoin_id', + ], + 'datatype' => [ + 'testmodeljoin_id' => 'number_natural', + 'testmodeljoin_created' => 'text_timestamp', + 'testmodeljoin_modified' => 'text_timestamp', + 'testmodeljoin_text' => 'text', + ], + 'connection' => 'default', + ]; - /** - * static configuration - * for usage in unit tests - * @var array - */ - public static $staticConfig = [ - 'field' => [ - 'testmodeljoin_id', - 'testmodeljoin_created', - 'testmodeljoin_modified', - 'testmodeljoin_text', - ], - 'primary' => [ - 'testmodeljoin_id' - ], - 'datatype' => [ - 'testmodeljoin_id' => 'number_natural', - 'testmodeljoin_created' => 'text_timestamp', - 'testmodeljoin_modified' => 'text_timestamp', - 'testmodeljoin_text' => 'text', - ], - 'connection' => 'default' - ]; + /** + * {@inheritDoc} + */ + public function __construct(array $modeldata = []) + { + parent::__construct('crudtest', 'testmodeljoin', static::$staticConfig); + } } diff --git a/tests/crud/model/testmodelwrongflag.php b/tests/crud/model/testmodelwrongflag.php index f807c01..5b7e926 100644 --- a/tests/crud/model/testmodelwrongflag.php +++ b/tests/crud/model/testmodelwrongflag.php @@ -1,45 +1,49 @@ [ + 'testmodelwrongflag_id', + 'testmodelwrongflag_created', + 'testmodelwrongflag_modified', + 'testmodelwrongflag_text', + 'testmodelwrongflag_flag', + ], + 'flag' => null, + 'primary' => [ + 'testmodelwrongflag_id', + ], + 'options' => [ + ], + 'datatype' => [ + 'testmodelwrongflag_id' => 'number_natural', + 'testmodelwrongflag_created' => 'text_timestamp', + 'testmodelwrongflag_modified' => 'text_timestamp', + 'testmodelwrongflag_text' => 'text', + 'testmodelwrongflag_flag' => 'number_natural', + ], + 'connection' => 'default', + ]; - /** - * static configuration - * for usage in unit tests - * @var array - */ - public static $staticConfig = [ - 'field' => [ - 'testmodelwrongflag_id', - 'testmodelwrongflag_created', - 'testmodelwrongflag_modified', - 'testmodelwrongflag_text', - 'testmodelwrongflag_flag', - ], - 'flag' => null, - 'primary' => [ - 'testmodelwrongflag_id' - ], - 'options' => [ - ], - 'datatype' => [ - 'testmodelwrongflag_id' => 'number_natural', - 'testmodelwrongflag_created' => 'text_timestamp', - 'testmodelwrongflag_modified' => 'text_timestamp', - 'testmodelwrongflag_text' => 'text', - 'testmodelwrongflag_flag' => 'number_natural', - ], - 'connection' => 'default' - ]; + /** + * {@inheritDoc} + */ + public function __construct(array $modeldata = []) + { + parent::__construct('crudtest', 'testmodelwrongflag', static::$staticConfig); + } } diff --git a/tests/crud/model/testmodelwrongforeign.php b/tests/crud/model/testmodelwrongforeign.php index e456424..3ae3884 100644 --- a/tests/crud/model/testmodelwrongforeign.php +++ b/tests/crud/model/testmodelwrongforeign.php @@ -1,63 +1,67 @@ [ - 'testmodelwrongforeign_id', - 'testmodelwrongforeign_created', - 'testmodelwrongforeign_modified', - 'testmodelwrongforeign_testmodeljoin_id', - 'testmodelwrongforeign_testmodeljoin', - 'testmodelwrongforeign_text', - ], - 'primary' => [ - 'testmodelwrongforeign_id' - ], - 'children' => [ - 'testmodelwrongforeign_testmodeljoin' => [ - 'type' => 'foreign', - 'field' => 'testmodelwrongforeign_testmodeljoin_id', - ] - ], - 'foreign' => [ - 'testmodelwrongforeign_testmodeljoin_id' => [ - // 'schema' => 'crudtest', - // 'model' => 'testmodeljoin', - // 'key' => 'testmodeljoin_id', - // 'display' => '{$element["testmodeljoin_text"]}' +class testmodelwrongforeign extends sqlModel +{ + /** + * static configuration + * for usage in unit tests + * @var array + */ + public static array $staticConfig = [ + 'field' => [ + 'testmodelwrongforeign_id', + 'testmodelwrongforeign_created', + 'testmodelwrongforeign_modified', + 'testmodelwrongforeign_testmodeljoin_id', + 'testmodelwrongforeign_testmodeljoin', + 'testmodelwrongforeign_text', + ], + 'primary' => [ + 'testmodelwrongforeign_id', + ], + 'children' => [ + 'testmodelwrongforeign_testmodeljoin' => [ + 'type' => 'foreign', + 'field' => 'testmodelwrongforeign_testmodeljoin_id', + ], ], - ], - 'required' => [ - 'testmodelwrongforeign_text' - ], - 'options' => [ - ], - 'datatype' => [ - 'testmodelwrongforeign_id' => 'number_natural', - 'testmodelwrongforeign_created' => 'text_timestamp', - 'testmodelwrongforeign_modified' => 'text_timestamp', - 'testmodelwrongforeign_testmodeljoin_id' => 'structure', - 'testmodelwrongforeign_testmodeljoin' => 'virtual', - 'testmodelwrongforeign_text' => 'text', - ], - 'connection' => 'default' - ]; + 'foreign' => [ + 'testmodelwrongforeign_testmodeljoin_id' => [ + // 'schema' => 'crudtest', + // 'model' => 'testmodeljoin', + // 'key' => 'testmodeljoin_id', + // 'display' => '{$element["testmodeljoin_text"]}' + ], + ], + 'required' => [ + 'testmodelwrongforeign_text', + ], + 'options' => [ + ], + 'datatype' => [ + 'testmodelwrongforeign_id' => 'number_natural', + 'testmodelwrongforeign_created' => 'text_timestamp', + 'testmodelwrongforeign_modified' => 'text_timestamp', + 'testmodelwrongforeign_testmodeljoin_id' => 'structure', + 'testmodelwrongforeign_testmodeljoin' => 'virtual', + 'testmodelwrongforeign_text' => 'text', + ], + 'connection' => 'default', + ]; + + /** + * {@inheritDoc} + */ + public function __construct(array $modeldata = []) + { + parent::__construct('crudtest', 'testmodelwrongforeign', static::$staticConfig); + } } diff --git a/tests/crud/model/testmodelwrongforeignfilter.php b/tests/crud/model/testmodelwrongforeignfilter.php index 01202bb..33d6733 100644 --- a/tests/crud/model/testmodelwrongforeignfilter.php +++ b/tests/crud/model/testmodelwrongforeignfilter.php @@ -1,66 +1,70 @@ [ - 'testmodelwrongforeignfilter_id', - 'testmodelwrongforeignfilter_created', - 'testmodelwrongforeignfilter_modified', - 'testmodelwrongforeignfilter_testmodeljoin_id', - 'testmodelwrongforeignfilter_testmodeljoin', - 'testmodelwrongforeignfilter_text', - ], - 'primary' => [ - 'testmodelwrongforeignfilter_id' - ], - 'children' => [ - 'testmodelwrongforeignfilter_testmodeljoin' => [ - 'type' => 'foreign', - 'field' => 'testmodelwrongforeignfilter_testmodeljoin_id', - ] - ], - 'foreign' => [ - 'testmodelwrongforeignfilter_testmodeljoin_id' => [ - 'schema' => 'crudtest', - 'model' => 'testmodeljoin', - 'key' => 'testmodeljoin_id', - 'display' => '{$element["testmodeljoin_text"]}', - 'filter' => [ - [] +class testmodelwrongforeignfilter extends sqlModel +{ + /** + * static configuration + * for usage in unit tests + * @var array + */ + public static array $staticConfig = [ + 'field' => [ + 'testmodelwrongforeignfilter_id', + 'testmodelwrongforeignfilter_created', + 'testmodelwrongforeignfilter_modified', + 'testmodelwrongforeignfilter_testmodeljoin_id', + 'testmodelwrongforeignfilter_testmodeljoin', + 'testmodelwrongforeignfilter_text', + ], + 'primary' => [ + 'testmodelwrongforeignfilter_id', + ], + 'children' => [ + 'testmodelwrongforeignfilter_testmodeljoin' => [ + 'type' => 'foreign', + 'field' => 'testmodelwrongforeignfilter_testmodeljoin_id', ], ], - ], - 'required' => [ - 'testmodelwrongforeignfilter_text' - ], - 'options' => [ - ], - 'datatype' => [ - 'testmodelwrongforeignfilter_id' => 'number_natural', - 'testmodelwrongforeignfilter_created' => 'text_timestamp', - 'testmodelwrongforeignfilter_modified' => 'text_timestamp', - 'testmodelwrongforeignfilter_testmodeljoin_id' => 'structure', - 'testmodelwrongforeignfilter_testmodeljoin' => 'virtual', - 'testmodelwrongforeignfilter_text' => 'text', - ], - 'connection' => 'default' - ]; + 'foreign' => [ + 'testmodelwrongforeignfilter_testmodeljoin_id' => [ + 'schema' => 'crudtest', + 'model' => 'testmodeljoin', + 'key' => 'testmodeljoin_id', + 'display' => '{$element["testmodeljoin_text"]}', + 'filter' => [ + [], + ], + ], + ], + 'required' => [ + 'testmodelwrongforeignfilter_text', + ], + 'options' => [ + ], + 'datatype' => [ + 'testmodelwrongforeignfilter_id' => 'number_natural', + 'testmodelwrongforeignfilter_created' => 'text_timestamp', + 'testmodelwrongforeignfilter_modified' => 'text_timestamp', + 'testmodelwrongforeignfilter_testmodeljoin_id' => 'structure', + 'testmodelwrongforeignfilter_testmodeljoin' => 'virtual', + 'testmodelwrongforeignfilter_text' => 'text', + ], + 'connection' => 'default', + ]; + + /** + * {@inheritDoc} + */ + public function __construct(array $modeldata = []) + { + parent::__construct('crudtest', 'testmodelwrongforeignfilter', static::$staticConfig); + } } diff --git a/tests/crud/model/testmodelwrongforeignorder.php b/tests/crud/model/testmodelwrongforeignorder.php index 2677180..5199ec2 100644 --- a/tests/crud/model/testmodelwrongforeignorder.php +++ b/tests/crud/model/testmodelwrongforeignorder.php @@ -1,66 +1,70 @@ [ - 'testmodelwrongforeignorder_id', - 'testmodelwrongforeignorder_created', - 'testmodelwrongforeignorder_modified', - 'testmodelwrongforeignorder_testmodeljoin_id', - 'testmodelwrongforeignorder_testmodeljoin', - 'testmodelwrongforeignorder_text', - ], - 'primary' => [ - 'testmodelwrongforeignorder_id' - ], - 'children' => [ - 'testmodelwrongforeignorder_testmodeljoin' => [ - 'type' => 'foreign', - 'field' => 'testmodelwrongforeignorder_testmodeljoin_id', - ] - ], - 'foreign' => [ - 'testmodelwrongforeignorder_testmodeljoin_id' => [ - 'schema' => 'crudtest', - 'model' => 'testmodeljoin', - 'key' => 'testmodeljoin_id', - 'display' => '{$element["testmodeljoin_text"]}', - 'order' => [ - [] +class testmodelwrongforeignorder extends sqlModel +{ + /** + * static configuration + * for usage in unit tests + * @var array + */ + public static array $staticConfig = [ + 'field' => [ + 'testmodelwrongforeignorder_id', + 'testmodelwrongforeignorder_created', + 'testmodelwrongforeignorder_modified', + 'testmodelwrongforeignorder_testmodeljoin_id', + 'testmodelwrongforeignorder_testmodeljoin', + 'testmodelwrongforeignorder_text', + ], + 'primary' => [ + 'testmodelwrongforeignorder_id', + ], + 'children' => [ + 'testmodelwrongforeignorder_testmodeljoin' => [ + 'type' => 'foreign', + 'field' => 'testmodelwrongforeignorder_testmodeljoin_id', ], ], - ], - 'required' => [ - 'testmodelwrongforeignorder_text' - ], - 'options' => [ - ], - 'datatype' => [ - 'testmodelwrongforeignorder_id' => 'number_natural', - 'testmodelwrongforeignorder_created' => 'text_timestamp', - 'testmodelwrongforeignorder_modified' => 'text_timestamp', - 'testmodelwrongforeignorder_testmodeljoin_id' => 'structure', - 'testmodelwrongforeignorder_testmodeljoin' => 'virtual', - 'testmodelwrongforeignorder_text' => 'text', - ], - 'connection' => 'default' - ]; + 'foreign' => [ + 'testmodelwrongforeignorder_testmodeljoin_id' => [ + 'schema' => 'crudtest', + 'model' => 'testmodeljoin', + 'key' => 'testmodeljoin_id', + 'display' => '{$element["testmodeljoin_text"]}', + 'order' => [ + [], + ], + ], + ], + 'required' => [ + 'testmodelwrongforeignorder_text', + ], + 'options' => [ + ], + 'datatype' => [ + 'testmodelwrongforeignorder_id' => 'number_natural', + 'testmodelwrongforeignorder_created' => 'text_timestamp', + 'testmodelwrongforeignorder_modified' => 'text_timestamp', + 'testmodelwrongforeignorder_testmodeljoin_id' => 'structure', + 'testmodelwrongforeignorder_testmodeljoin' => 'virtual', + 'testmodelwrongforeignorder_text' => 'text', + ], + 'connection' => 'default', + ]; + + /** + * {@inheritDoc} + */ + public function __construct(array $modeldata = []) + { + parent::__construct('crudtest', 'testmodelwrongforeignorder', static::$staticConfig); + } } diff --git a/tests/fieldTest.php b/tests/fieldTest.php index b1e84d4..d1adca5 100644 --- a/tests/fieldTest.php +++ b/tests/fieldTest.php @@ -1,304 +1,349 @@ expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_CONSTRUCT_CONFIGURATIONINVALID'); + + new field([]); + } - /** - * @inheritDoc - */ - protected function setUp(): void - { - parent::setUp(); - - $app = static::createApp(); - overrideableApp::__injectApp([ - 'vendor' => 'codename', - 'app' => 'core-ui', - 'namespace' => '\\codename\\core\\ui' - ]); - - $app->getAppstack(); - - static::setEnvironmentConfig([ - 'test' => [ - 'cache' => [ - 'default' => [ - 'driver' => 'memory' - ] - ], - 'translate' => [ - 'default' => [ - 'driver' => 'json', - 'inherit' => true, - ] - ], - 'templateengine' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - ] - ]); - } - - /** - * [testInvalidConfiguration description] - */ - public function testInvalidConfiguration(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_CONSTRUCT_CONFIGURATIONINVALID'); - - $field = new \codename\core\ui\field([]); - } - - /** - * [testConfiguration description] - */ - public function testConfiguration(): void { - $field = new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_value' => 'example', - ]); - - $result = $field->getConfig(); - $this->assertEquals([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_id' => 'example', - 'field_fieldtype' => 'input', - 'field_class' => 'input', - 'field_required' => false, - 'field_readonly' => false, - 'field_ajax' => false, - 'field_noninput' => false, - 'field_placeholder' => '', - 'field_value' => 'example', - 'field_datatype' => 'text', - 'field_validator' => '', - 'field_description' => '', - 'field_title' => '', - ], $result->get()); - - $this->assertFalse($field->isRequired()); - - $field->setProperty('field_required', true); - - $this->assertTrue($field->getProperty('field_required')); - - $field->setValue('example_editing'); - - $result = $field->getConfig(); - $this->assertEquals([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_id' => 'example', - 'field_fieldtype' => 'input', - 'field_class' => 'input', - 'field_required' => true, - 'field_readonly' => false, - 'field_ajax' => false, - 'field_noninput' => false, - 'field_placeholder' => '', - 'field_value' => 'example_editing', - 'field_datatype' => 'text', - 'field_validator' => '', - 'field_description' => '', - 'field_title' => '', - ], $result->get()); - - } - - /** - * [testProperties description] - */ - public function testProperties(): void { - $properties = \codename\core\ui\field::getProperties(); - - $this->assertEquals([ - 'field_id', - 'field_name', - 'field_title', - 'field_description', - 'field_type', - 'field_required', - 'field_placeholder', - 'field_multiple', - 'field_ajax', - 'field_noninput', - ], $properties); - - } - - /** - * [testNormalizedFieldValue description] - */ - public function testNormalizedFieldValue(): void { - $fields = [ - [ 'name' => 'example', 'value' => true, 'datatype' => 'boolean', 'result' => true ], - [ 'name' => 'example', 'value' => 1, 'datatype' => 'boolean', 'result' => true ], - - [ 'name' => 'example', 'value' => '', 'datatype' => 'boolean', 'result' => null ], - [ 'name' => 'example', 'value' => '1', 'datatype' => 'boolean', 'result' => true ], - [ 'name' => 'example', 'value' => '0', 'datatype' => 'boolean', 'result' => false ], - [ 'name' => 'example', 'value' => 'true', 'datatype' => 'boolean', 'result' => true ], - [ 'name' => 'example', 'value' => 'false', 'datatype' => 'boolean', 'result' => false ], - - [ 'name' => 'example', 'value' => '', 'datatype' => 'number_natural', 'result' => null ], - [ 'name' => 'example', 'value' => '1', 'datatype' => 'number_natural', 'result' => 1 ], - [ 'name' => 'example', 'value' => null, 'datatype' => 'number_natural', 'result' => null ], - [ 'name' => 'example', 'value' => 1, 'datatype' => 'number_natural', 'result' => 1 ], - ]; - - foreach($fields as $field) { - $result = \codename\core\ui\field::getNormalizedFieldValue($field['name'], $field['value'], $field['datatype']); - $this->assertEquals($field['result'], $result); + /** + * [testConfiguration description] + * @throws ReflectionException + * @throws exception + */ + public function testConfiguration(): void + { + $field = new field([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_value' => 'example', + ]); + + $result = $field->getConfig(); + static::assertEquals([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_id' => 'example', + 'field_fieldtype' => 'input', + 'field_class' => 'input', + 'field_required' => false, + 'field_readonly' => false, + 'field_ajax' => false, + 'field_noninput' => false, + 'field_placeholder' => '', + 'field_value' => 'example', + 'field_datatype' => 'text', + 'field_validator' => '', + 'field_description' => '', + 'field_title' => '', + ], $result->get()); + + static::assertFalse($field->isRequired()); + + $field->setProperty('field_required', true); + + static::assertTrue($field->getConfig()->get('field_required')); + + $field->setValue('example_editing'); + + $result = $field->getConfig(); + static::assertEquals([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_id' => 'example', + 'field_fieldtype' => 'input', + 'field_class' => 'input', + 'field_required' => true, + 'field_readonly' => false, + 'field_ajax' => false, + 'field_noninput' => false, + 'field_placeholder' => '', + 'field_value' => 'example_editing', + 'field_datatype' => 'text', + 'field_validator' => '', + 'field_description' => '', + 'field_title' => '', + ], $result->get()); } - } - - /** - * [testNormalizedFieldValueInvalidValueCase1 description] - */ - public function testNormalizedFieldValueInvalidValueCase1(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_FIELD_NORMALIZEFIELD_BOOLEAN_INVALID'); - $result = \codename\core\ui\field::getNormalizedFieldValue('example', 2, 'boolean'); - } - - /** - * [testNormalizedFieldValueInvalidValueCase1 description] - */ - public function testNormalizedFieldValueInvalidValueCase2(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_FIELD_NORMALIZEFIELD_BOOLEAN_INVALID'); - $result = \codename\core\ui\field::getNormalizedFieldValue('example', '2', 'boolean'); - } - - /** - * [testOutputData description] - */ - public function testOutputData(): void { - $field = new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_value' => 'example', - ]); - - $result = $field->output(true); - $this->assertEquals([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_id' => 'example', - 'field_fieldtype' => 'input', - 'field_class' => 'input', - 'field_required' => false, - 'field_readonly' => false, - 'field_ajax' => false, - 'field_noninput' => false, - 'field_placeholder' => '', - 'field_value' => 'example', - 'field_datatype' => 'text', - 'field_validator' => '', - 'field_description' => '', - 'field_title' => '', - ], $result); - - } - - /** - * [testOutputData description] - */ - public function testOutput(): void { - $field = new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_value' => function() { - return 'example'; - }, - 'field_idfield' => 'id', - 'field_displayfield' => 'id', - 'field_valuefield' => 'name', - 'field_elements' => function() { - return [ - [ - 'id' => 1, - 'name' => 'name', - ] - ]; - }, - ]); - - $field->setType('default'); - - $output = $field->output(); - $this->assertEquals('frontend/field/default/text', $output); - - } - - /** - * [testOutputCase2 description] - */ - public function testOutputCase2(): void { - $field = new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_value' => function() { - return 'example'; - }, - 'field_displayfield' => 'id', - 'field_valuefield' => 'name', - 'field_elements' => function() { - return [ - [ - 'id' => 1, - 'name' => 'name', - ] + /** + * [testProperties description] + */ + public function testProperties(): void + { + $properties = field::getProperties(); + + static::assertEquals([ + 'field_id', + 'field_name', + 'field_title', + 'field_description', + 'field_type', + 'field_required', + 'field_placeholder', + 'field_multiple', + 'field_ajax', + 'field_noninput', + ], $properties); + } + + /** + * [testNormalizedFieldValue description] + * @throws exception + */ + public function testNormalizedFieldValue(): void + { + $fields = [ + ['name' => 'example', 'value' => true, 'datatype' => 'boolean', 'result' => true], + ['name' => 'example', 'value' => 1, 'datatype' => 'boolean', 'result' => true], + + ['name' => 'example', 'value' => '', 'datatype' => 'boolean', 'result' => null], + ['name' => 'example', 'value' => '1', 'datatype' => 'boolean', 'result' => true], + ['name' => 'example', 'value' => '0', 'datatype' => 'boolean', 'result' => false], + ['name' => 'example', 'value' => 'true', 'datatype' => 'boolean', 'result' => true], + ['name' => 'example', 'value' => 'false', 'datatype' => 'boolean', 'result' => false], + + ['name' => 'example', 'value' => '', 'datatype' => 'number_natural', 'result' => null], + ['name' => 'example', 'value' => '1', 'datatype' => 'number_natural', 'result' => 1], + ['name' => 'example', 'value' => null, 'datatype' => 'number_natural', 'result' => null], + ['name' => 'example', 'value' => 1, 'datatype' => 'number_natural', 'result' => 1], ]; - }, - ]); - - $field->setType('default'); - - $output = $field->output(); - $this->assertEquals('frontend/field/default/text', $output); - - } - - /** - * [testOutputDataWithForm description] - */ - public function testOutputDataWithForm(): void { - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ]); - - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'formfieldexample', - 'field_type' => 'text', - 'field_value' => 'formfieldexample', - ])); - - $field = new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'form', - 'field_value' => [ - 'formfieldexample' => 'formfieldexample', - ], - 'form' => $form, - ]); - - $result = $field->output(true); - $this->assertEquals([ - 'formfieldexample' => 'formfieldexample', - ], $result['field_value']); - - } + foreach ($fields as $field) { + $result = field::getNormalizedFieldValue($field['name'], $field['value'], $field['datatype']); + static::assertEquals($field['result'], $result); + } + } + + /** + * [testNormalizedFieldValueInvalidValueCase1 description] + */ + public function testNormalizedFieldValueInvalidValueCase1(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_FIELD_NORMALIZEFIELD_BOOLEAN_INVALID'); + field::getNormalizedFieldValue('example', 2, 'boolean'); + } + + /** + * [testNormalizedFieldValueInvalidValueCase1 description] + */ + public function testNormalizedFieldValueInvalidValueCase2(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_FIELD_NORMALIZEFIELD_BOOLEAN_INVALID'); + field::getNormalizedFieldValue('example', '2', 'boolean'); + } + + /** + * [testOutputData description] + * @throws ReflectionException + * @throws exception + */ + public function testOutputData(): void + { + $field = new field([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_value' => 'example', + ]); + + $result = $field->output(true); + static::assertEquals([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_id' => 'example', + 'field_fieldtype' => 'input', + 'field_class' => 'input', + 'field_required' => false, + 'field_readonly' => false, + 'field_ajax' => false, + 'field_noninput' => false, + 'field_placeholder' => '', + 'field_value' => 'example', + 'field_datatype' => 'text', + 'field_validator' => '', + 'field_description' => '', + 'field_title' => '', + ], $result); + } + + /** + * [testOutputData description] + * @throws ReflectionException + * @throws exception + */ + public function testOutput(): void + { + $field = new field([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_value' => function () { + return 'example'; + }, + 'field_idfield' => 'id', + 'field_displayfield' => 'id', + 'field_valuefield' => 'name', + 'field_elements' => function () { + return [ + [ + 'id' => 1, + 'name' => 'name', + ], + ]; + }, + ]); + + $field->setType('default'); + + $output = $field->output(); + static::assertEquals('frontend/field/default/text', $output); + } + + /** + * [testOutputCase2 description] + * @throws ReflectionException + * @throws exception + */ + public function testOutputCase2(): void + { + $field = new field([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_value' => function () { + return 'example'; + }, + 'field_displayfield' => 'id', + 'field_valuefield' => 'name', + 'field_elements' => function () { + return [ + [ + 'id' => 1, + 'name' => 'name', + ], + ]; + }, + ]); + + $field->setType('default'); + + $output = $field->output(); + static::assertEquals('frontend/field/default/text', $output); + } + + /** + * [testOutputDataWithForm description] + * @throws ReflectionException + * @throws exception + */ + public function testOutputDataWithForm(): void + { + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ]); + + $form->addField( + new field([ + 'field_name' => 'formfieldexample', + 'field_type' => 'text', + 'field_value' => 'formfieldexample', + ]) + ); + + $field = new field([ + 'field_name' => 'example', + 'field_type' => 'form', + 'field_value' => [ + 'formfieldexample' => 'formfieldexample', + ], + 'form' => $form, + ]); + + $result = $field->output(true); + static::assertEquals([ + 'formfieldexample' => 'formfieldexample', + ], $result['field_value']); + } + + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws ErrorException + * @throws Throwable + * @throws compileErrorException + * @throws coreErrorException + * @throws coreWarningException + * @throws parseException + * @throws recoverableErrorException + * @throws strictException + * @throws userErrorException + * @throws userWarningException + * @throws warningException + * @throws exception + */ + protected function setUp(): void + { + parent::setUp(); + + $app = static::createApp(); + overrideableApp::__injectApp([ + 'vendor' => 'codename', + 'app' => 'core-ui', + 'namespace' => '\\codename\\core\\ui', + ]); + + $app::getAppstack(); + + static::setEnvironmentConfig([ + 'test' => [ + 'cache' => [ + 'default' => [ + 'driver' => 'memory', + ], + ], + 'translate' => [ + 'default' => [ + 'driver' => 'json', + 'inherit' => true, + ], + ], + 'templateengine' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + ], + ]); + } } diff --git a/tests/fieldsetTest.php b/tests/fieldsetTest.php index 6820003..3296243 100644 --- a/tests/fieldsetTest.php +++ b/tests/fieldsetTest.php @@ -1,191 +1,239 @@ 'codename', - 'app' => 'core-ui', - 'namespace' => '\\codename\\core\\ui' - ]); - - $app->getAppstack(); - - static::setEnvironmentConfig([ - 'test' => [ - 'cache' => [ - 'default' => [ - 'driver' => 'memory' - ] - ], - 'translate' => [ - 'default' => [ - 'driver' => 'json', - 'inherit' => true, - ] - ], - 'templateengine' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - ] - ]); - } - - /** - * [testGeneric description] - */ - public function testGeneric(): void { - $fieldset = new \codename\core\ui\fieldset([ - 'fieldset_id' => 'example', - 'fieldset_name' => 'example', - ]); - - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_value' => 'example', - ])); - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'example2', - 'field_type' => 'text', - 'field_value' => 'example2', - ]), 0); - - $config = $fieldset->output(true); - - $this->assertEquals('example', $config['fieldset_id']); - $this->assertEquals('FIELDSET_EXAMPLE', $config['fieldset_name']); - - $this->assertCount(2, $config['fields'] ?? []); - $this->assertInstanceOf(\codename\core\ui\field::class, $config['fields'][0]); - $this->assertInstanceOf(\codename\core\ui\field::class, $config['fields'][1]); - - $this->assertEquals([ - 'field_name' => 'example2', - 'field_type' => 'text', - 'field_id' => 'example2', - 'field_fieldtype' => 'input', - 'field_class' => 'input', - 'field_required' => false, - 'field_readonly' => false, - 'field_ajax' => false, - 'field_noninput' => false, - 'field_placeholder' => '', - 'field_value' => 'example2', - 'field_datatype' => 'text', - 'field_validator' => '', - 'field_description' => '', - 'field_title' => '', - ], $config['fields'][0]->getConfig()->get()); - - $fields = $fieldset->getFields(); - $this->assertCount(2, $fields ?? []); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[0]); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[1]); - - $this->assertEquals([ - 'field_name' => 'example2', - 'field_type' => 'text', - 'field_id' => 'example2', - 'field_fieldtype' => 'input', - 'field_class' => 'input', - 'field_required' => false, - 'field_readonly' => false, - 'field_ajax' => false, - 'field_noninput' => false, - 'field_placeholder' => '', - 'field_value' => 'example2', - 'field_datatype' => 'text', - 'field_validator' => '', - 'field_description' => '', - 'field_title' => '', - ], $fields[0]->getConfig()->get()); - } - - /** - * [testFieldsetNameOverride description] - */ - public function testFieldsetNameOverride(): void { - $fieldset = new \codename\core\ui\fieldset([ - 'fieldset_id' => 'example', - 'fieldset_name' => 'example', - 'fieldset_name_override' => 'example_override', - ]); - - $config = $fieldset->output(true); - - $this->assertEquals('example', $config['fieldset_id']); - $this->assertEquals('example_override', $config['fieldset_name']); - - $this->assertCount(0, $config['fields'] ?? []); - } - - /** - * [testOutput description] - */ - public function testOutput(): void { - $fieldset = new \codename\core\ui\fieldset([ - 'fieldset_id' => 'example', - 'fieldset_name' => 'example', - 'fieldset_name_override' => 'example_override', - ]); - - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_value' => 'example', - ])); - - $fieldset->setType('default'); - - $output = $fieldset->output(); - $this->assertEquals('frontend/fieldset/default', $output); - } - - /** - * Tests \JsonSerializable Interface integrity - */ - public function testJsonSerialize(): void { - $fieldset = new \codename\core\ui\fieldset([ - 'fieldset_id' => 'example', - 'fieldset_name' => 'example', - 'fieldset_name_override' => 'example_override', - ]); - - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_value' => 'example', - ])); - - $fieldset->setType('default'); - - $outputData = $fieldset->output(true); - - $jsonData = json_decode(json_encode($fieldset), true); - - $this->assertEquals('example', $jsonData['fieldset_id']); - $this->assertEquals('example_override', $jsonData['fieldset_name']); - - $this->assertCount(1, $jsonData['fields']); - $fieldData = $jsonData['fields'][0]; - $this->assertEquals('example', $fieldData['field_name']); - $this->assertEquals('text', $fieldData['field_type']); - $this->assertEquals('example', $fieldData['field_value']); - } - + /** + * [testGeneric description] + * @throws ReflectionException + * @throws exception + */ + public function testGeneric(): void + { + $fieldset = new fieldset([ + 'fieldset_id' => 'example', + 'fieldset_name' => 'example', + ]); + + $fieldset->addField( + new field([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_value' => 'example', + ]) + ); + $fieldset->addField( + new field([ + 'field_name' => 'example2', + 'field_type' => 'text', + 'field_value' => 'example2', + ]), + 0 + ); + + $config = $fieldset->output(true); + + static::assertEquals('example', $config['fieldset_id']); + static::assertEquals('FIELDSET_EXAMPLE', $config['fieldset_name']); + + static::assertCount(2, $config['fields'] ?? []); + static::assertInstanceOf(field::class, $config['fields'][0]); + static::assertInstanceOf(field::class, $config['fields'][1]); + + static::assertEquals([ + 'field_name' => 'example2', + 'field_type' => 'text', + 'field_id' => 'example2', + 'field_fieldtype' => 'input', + 'field_class' => 'input', + 'field_required' => false, + 'field_readonly' => false, + 'field_ajax' => false, + 'field_noninput' => false, + 'field_placeholder' => '', + 'field_value' => 'example2', + 'field_datatype' => 'text', + 'field_validator' => '', + 'field_description' => '', + 'field_title' => '', + ], $config['fields'][0]->getConfig()->get()); + + $fields = $fieldset->getFields(); + static::assertCount(2, $fields); + static::assertInstanceOf(field::class, $fields[0]); + static::assertInstanceOf(field::class, $fields[1]); + + static::assertEquals([ + 'field_name' => 'example2', + 'field_type' => 'text', + 'field_id' => 'example2', + 'field_fieldtype' => 'input', + 'field_class' => 'input', + 'field_required' => false, + 'field_readonly' => false, + 'field_ajax' => false, + 'field_noninput' => false, + 'field_placeholder' => '', + 'field_value' => 'example2', + 'field_datatype' => 'text', + 'field_validator' => '', + 'field_description' => '', + 'field_title' => '', + ], $fields[0]->getConfig()->get()); + } + + /** + * [testFieldsetNameOverride description] + * @throws ReflectionException + * @throws exception + */ + public function testFieldsetNameOverride(): void + { + $fieldset = new fieldset([ + 'fieldset_id' => 'example', + 'fieldset_name' => 'example', + 'fieldset_name_override' => 'example_override', + ]); + + $config = $fieldset->output(true); + + static::assertEquals('example', $config['fieldset_id']); + static::assertEquals('example_override', $config['fieldset_name']); + + static::assertCount(0, $config['fields'] ?? []); + } + + /** + * [testOutput description] + * @throws ReflectionException + * @throws exception + */ + public function testOutput(): void + { + $fieldset = new fieldset([ + 'fieldset_id' => 'example', + 'fieldset_name' => 'example', + 'fieldset_name_override' => 'example_override', + ]); + + $fieldset->addField( + new field([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_value' => 'example', + ]) + ); + + $fieldset->setType('default'); + + $output = $fieldset->output(); + static::assertEquals('frontend/fieldset/default', $output); + } + + /** + * Tests \JsonSerializable Interface integrity + * @throws ReflectionException + * @throws exception + */ + public function testJsonSerialize(): void + { + $fieldset = new fieldset([ + 'fieldset_id' => 'example', + 'fieldset_name' => 'example', + 'fieldset_name_override' => 'example_override', + ]); + + $fieldset->addField( + new field([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_value' => 'example', + ]) + ); + + $fieldset->setType('default'); + + $fieldset->output(true); + + $jsonData = json_decode(json_encode($fieldset), true); + + static::assertEquals('example', $jsonData['fieldset_id']); + static::assertEquals('example_override', $jsonData['fieldset_name']); + + static::assertCount(1, $jsonData['fields']); + $fieldData = $jsonData['fields'][0]; + static::assertEquals('example', $fieldData['field_name']); + static::assertEquals('text', $fieldData['field_type']); + static::assertEquals('example', $fieldData['field_value']); + } + + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws ErrorException + * @throws Throwable + * @throws compileErrorException + * @throws coreErrorException + * @throws coreWarningException + * @throws parseException + * @throws recoverableErrorException + * @throws strictException + * @throws userErrorException + * @throws userWarningException + * @throws warningException + * @throws exception + */ + protected function setUp(): void + { + parent::setUp(); + + $app = static::createApp(); + overrideableApp::__injectApp([ + 'vendor' => 'codename', + 'app' => 'core-ui', + 'namespace' => '\\codename\\core\\ui', + ]); + + $app::getAppstack(); + + static::setEnvironmentConfig([ + 'test' => [ + 'cache' => [ + 'default' => [ + 'driver' => 'memory', + ], + ], + 'translate' => [ + 'default' => [ + 'driver' => 'json', + 'inherit' => true, + ], + ], + 'templateengine' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + ], + ]); + } } diff --git a/tests/formTest.php b/tests/formTest.php index 5f4cebe..0572d44 100644 --- a/tests/formTest.php +++ b/tests/formTest.php @@ -1,734 +1,863 @@ 'codename', - 'app' => 'core-ui', - 'namespace' => '\\codename\\core\\ui' - ]); - - $app->getAppstack(); - - static::setEnvironmentConfig([ - 'test' => [ - 'cache' => [ - 'default' => [ - 'driver' => 'memory' - ] - ], - 'translate' => [ - 'default' => [ - 'driver' => 'json', - 'inherit' => true, - ] - ], - 'templateengine' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - ] - ]); - } - - /** - * [testInvalidConstruct description] - */ - public function testInvalidConstruct(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_CONSTRUCT_CONFIGURATIONINVALID'); - - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'post', - ]); - - } - - /** - * [testEmptyForm description] - */ - public function testEmptyForm(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_OUTPUT_FORMISEMPTY'); - - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'post', - 'form_method' => '', - ]); - - $form->output(true); - - } - - /** - * [testGeneric description] - */ - public function testGeneric(): void { - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'post', - 'form_method' => '', - ]); - - $this->assertEquals([ - 'form_id' => 'exampleform', - 'form_action' => 'post', - 'form_method' => '', - ], $form->config); - $this->assertCount(0, $form->getFields()); - $this->assertCount(0, $form->getFieldsets()); - $this->assertEmpty($form->getErrorstack()->getErrors()); - - $this->assertEmpty($form->getTemplateEngine()); - - $templateEngine = overrideableApp::getTemplateEngine(); - $form->setTemplateEngine($templateEngine); - $this->assertNotEmpty($form->getTemplateEngine()); - - } - - /** - * [testInvalidFormWithFields description] - */ - public function testInvalidFormWithFields(): void { - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ]); - - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'invalidexample', - 'field_type' => 'text', - 'field_value' => null, - 'field_required' => true, - ])); - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'noninputexample', - 'field_type' => 'text', - 'field_value' => null, - 'field_noninput' => true, - ])); - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'invalidexample2', - 'field_type' => 'text', - 'field_value' => 'invalidexample2', - ]), 0); - - $fields = $form->getFields(); - $this->assertCount(3, $fields ?? []); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[0]); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[1]); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[2]); - - $this->assertEquals([ - 'field_name' => 'invalidexample2', - 'field_type' => 'text', - 'field_id' => 'invalidexample2', - 'field_fieldtype' => 'input', - 'field_class' => 'input', - 'field_required' => false, - 'field_readonly' => false, - 'field_ajax' => false, - 'field_noninput' => false, - 'field_placeholder' => '', - 'field_value' => 'invalidexample2', - 'field_datatype' => 'text', - 'field_validator' => '', - 'field_description' => '', - 'field_title' => '', - ], $fields[0]->getConfig()->get()); - - $form->setId('invalidexampleform'); - $form->setAction('post'); - - $this->assertEquals([ - 'form_id' => 'core_form_invalidexampleform', - 'form_action' => 'post', - 'form_method' => '', - ], $form->config); - $this->assertCount(3, $form->getFields()); - $this->assertCount(0, $form->getFieldsets()); - $this->assertEmpty($form->getErrorstack()->getErrors()); - - $this->assertFalse($form->isSent()); - - $result = $form->output(true); - $this->assertNotEmpty($result); - - overrideableApp::getRequest()->setData('formSentcore_form_invalidexampleform', true); - - $this->assertTrue($form->isSent()); - - $this->assertFalse($form->isValid()); - - $errors = $form->getErrorstack()->getErrors(); - - $this->assertCount(1, $errors); - $this->assertEquals([ - [ - '__IDENTIFIER' => 'invalidexample', - '__CODE' => 'VALIDATION.FIELD_NOT_SET', - '__TYPE' => 'VALIDATION', - '__DETAILS' => null, - ] - ], $errors); - - } - - /** - * [testValidFormWithFields description] - */ - public function testValidFormWithFields(): void { - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ]); - - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'validfieldexample', - 'field_type' => 'text', - 'field_value' => 'validfieldexample', - 'field_required' => true, - ])); - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'validfieldexample2', - 'field_type' => 'text', - 'field_value' => 'validfieldexample2', - ]), 0); - - $fields = $form->getFields(); - $this->assertCount(2, $fields ?? []); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[0]); - $this->assertInstanceOf(\codename\core\ui\field::class, $fields[1]); - - $this->assertEquals([ - 'field_name' => 'validfieldexample2', - 'field_type' => 'text', - 'field_id' => 'validfieldexample2', - 'field_fieldtype' => 'input', - 'field_class' => 'input', - 'field_required' => false, - 'field_readonly' => false, - 'field_ajax' => false, - 'field_noninput' => false, - 'field_placeholder' => '', - 'field_value' => 'validfieldexample2', - 'field_datatype' => 'text', - 'field_validator' => '', - 'field_description' => '', - 'field_title' => '', - ], $fields[0]->getConfig()->get()); - - $form->setId('validfieldexampleform'); - $form->setAction('post'); - - $this->assertEquals([ - 'form_id' => 'core_form_validfieldexampleform', - 'form_action' => 'post', - 'form_method' => '', - ], $form->config); - $this->assertCount(2, $form->getFields()); - $this->assertCount(0, $form->getFieldsets()); - $this->assertEmpty($form->getErrorstack()->getErrors()); - - $this->assertFalse($form->isSent()); - - overrideableApp::getRequest()->setData('formSentcore_form_validfieldexampleform', true); - overrideableApp::getRequest()->setData('validfieldexample', 'validfieldexample'); - - $this->assertTrue($form->isSent()); - $this->assertTrue($form->isValid()); - - $errors = $form->getErrorstack()->getErrors(); - $this->assertCount(0, $errors); - $this->assertEmpty($errors); - - $data = $form->getData(); - $data = $form->normalizeData($data); - $this->assertEquals([ - 'validfieldexample' => 'validfieldexample' - ], $data); - - } - - /** - * [testInvalidFormWithFields description] - */ - public function testValidFormWithFieldset(): void { - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ]); - - $fieldset = new \codename\core\ui\fieldset([ - 'fieldset_id' => 'example', - 'fieldset_name' => 'example', - 'fieldset_name_override' => 'example', - ]); - - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'validexample', - 'field_type' => 'text', - 'field_value' => null, - 'field_required' => true, - ])); - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'validexample2', - 'field_type' => 'text', - 'field_value' => 'validexample2', - ]), 0); - - $form->addFieldset($fieldset); - - $fieldsets = $form->getFieldsets(); - $this->assertCount(1, $fieldsets ?? []); - $this->assertInstanceOf(\codename\core\ui\fieldset::class, $fieldsets[0]); - - $form->setId('validexampleform'); - $form->setAction('post'); - - $this->assertEquals([ - 'form_id' => 'core_form_validexampleform', - 'form_action' => 'post', - 'form_method' => '', - ], $form->config); - $this->assertCount(0, $form->getFields()); - $this->assertCount(1, $form->getFieldsets()); - $this->assertEmpty($form->getErrorstack()->getErrors()); - - $this->assertFalse($form->isSent()); - - overrideableApp::getRequest()->setData('formSentcore_form_validexampleform', true); - overrideableApp::getRequest()->setData('validexample', 'validexample'); - - $this->assertTrue($form->isSent()); - $this->assertTrue($form->isValid()); - - $errors = $form->getErrorstack()->getErrors(); - $this->assertCount(0, $errors); - $this->assertEmpty($errors); - - $data = $form->getData(); - // NOTE: returned null, if not fields is set - $data = $form->normalizeData($data); - $this->assertEmpty($data); - - } - - /** - * [testFormOutput description] - */ - public function testFormOutput(): void { - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ]); - - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'fieldexample', - 'field_type' => 'text', - 'field_value' => 'fieldexample', - ])); - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'fieldexample2', - 'field_type' => 'text', - 'field_value' => 'fieldexample2', - ])); - - $fieldset = new \codename\core\ui\fieldset([ - 'fieldset_id' => 'example', - 'fieldset_name' => 'example', - 'fieldset_name_override' => 'example', - ]); - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'fieldsetexample', - 'field_type' => 'text', - 'field_value' => 'fieldsetexample', - ])); - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'fieldsetexample2', - 'field_type' => 'text', - 'field_value' => 'fieldsetexample2', - ]), 0); - $form->addFieldset($fieldset); - - $fieldset->setType('default'); - - $output = $form->output(); - $this->assertEquals('frontend/form/default/form', $output); - - } - - /** - * [testFormSearchFieldByGetField description] - */ - public function testFormSearchFieldByGetField(): void { - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ]); - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'validfieldexample', - 'field_type' => 'text', - 'field_value' => 'validfieldexample', - 'field_required' => true, - ])); - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'validfieldexample2', - 'field_type' => 'text', - 'field_value' => 'validfieldexample2', - ]), 0); - $fieldset = new \codename\core\ui\fieldset([ - 'fieldset_id' => 'example', - 'fieldset_name' => 'example', - 'fieldset_name_override' => 'example', - ]); - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'validexample', - 'field_type' => 'text', - 'field_value' => null, - 'field_required' => true, - ])); - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'validexample2', - 'field_type' => 'text', - 'field_value' => 'validexample2', - ]), 0); - $form->addFieldset($fieldset); - - $field = $form->getField('validfieldexample2'); - $this->assertEquals([ - 'field_name' => 'validfieldexample2', - 'field_type' => 'text', - 'field_id' => 'validfieldexample2', - 'field_fieldtype' => 'input', - 'field_class' => 'input', - 'field_required' => false, - 'field_readonly' => false, - 'field_ajax' => false, - 'field_noninput' => false, - 'field_placeholder' => '', - 'field_value' => 'validfieldexample2', - 'field_datatype' => 'text', - 'field_validator' => '', - 'field_description' => '', - 'field_title' => '', - ], $field->getConfig()->get()); - - $field = $form->getField('validexample2'); - $this->assertEquals([ - 'field_name' => 'validexample2', - 'field_type' => 'text', - 'field_id' => 'validexample2', - 'field_fieldtype' => 'input', - 'field_class' => 'input', - 'field_required' => false, - 'field_readonly' => false, - 'field_ajax' => false, - 'field_noninput' => false, - 'field_placeholder' => '', - 'field_value' => 'validexample2', - 'field_datatype' => 'text', - 'field_validator' => '', - 'field_description' => '', - 'field_title' => '', - ], $field->getConfig()->get()); - - $field = $form->getField('fieldnotfound'); - $this->assertNull($field); - - } - - /** - * [testFormSearchFieldByGetFieldRecursive description] - */ - public function testFormSearchFieldByGetFieldRecursive(): void { - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ]); - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'validfieldexample', - 'field_type' => 'text', - 'field_value' => 'validfieldexample', - 'field_required' => true, - ])); - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'validfieldexample2', - 'field_type' => 'text', - 'field_value' => 'validfieldexample2', - ]), 0); - $fieldset = new \codename\core\ui\fieldset([ - 'fieldset_id' => 'example', - 'fieldset_name' => 'example', - 'fieldset_name_override' => 'example', - ]); - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'validexample', - 'field_type' => 'text', - 'field_value' => null, - 'field_required' => true, - ])); - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'validexample2', - 'field_type' => 'text', - 'field_value' => 'validexample2', - ]), 0); - $form->addFieldset($fieldset); - - $field = $form->getFieldRecursive([ 'validfieldexample2' ]); - $this->assertEquals([ - 'field_name' => 'validfieldexample2', - 'field_type' => 'text', - 'field_id' => 'validfieldexample2', - 'field_fieldtype' => 'input', - 'field_class' => 'input', - 'field_required' => false, - 'field_readonly' => false, - 'field_ajax' => false, - 'field_noninput' => false, - 'field_placeholder' => '', - 'field_value' => 'validfieldexample2', - 'field_datatype' => 'text', - 'field_validator' => '', - 'field_description' => '', - 'field_title' => '', - ], $field->getConfig()->get()); - - $field = $form->getFieldRecursive([ 'validexample2' ]); - $this->assertEquals([ - 'field_name' => 'validexample2', - 'field_type' => 'text', - 'field_id' => 'validexample2', - 'field_fieldtype' => 'input', - 'field_class' => 'input', - 'field_required' => false, - 'field_readonly' => false, - 'field_ajax' => false, - 'field_noninput' => false, - 'field_placeholder' => '', - 'field_value' => 'validexample2', - 'field_datatype' => 'text', - 'field_validator' => '', - 'field_description' => '', - 'field_title' => '', - ], $field->getConfig()->get()); - - $field = $form->getFieldRecursive([ 'fieldnotfound' ]); - $this->assertNull($field); - - } - - /** - * [testFormFieldWithoutForm description] - */ - public function testFormFieldWithoutForm(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('FORM_GETFIELDRECURSIVE_NO_FORM_INSTANCE'); - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ]); - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_value' => 'example', - ])); - - $field = $form->getFieldRecursive([ 'example', 'example' ]); - } - - /** - * [testFormFieldWithoutFormInstance description] - */ - public function testFormFieldWithoutFormInstance(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('FORM_GETFIELDRECURSIVE_INVALID_FORM_INSTANCE'); - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ]); - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_value' => 'example', - 'form' => 'example', - ])); - - $field = $form->getFieldRecursive([ 'example', 'example' ]); - } - - /** - * [testFormFieldWithFormInstance description] - */ - public function testFormFieldWithFormInstance(): void { - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ]); - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_value' => 'example', - 'form' => (new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ])), - ])); - - $field = $form->getFieldRecursive([ 'example', 'example' ]); - $this->assertNull($field); - } - - /** - * [testFormFieldsetWithoutForm description] - */ - public function testFormFieldsetWithoutForm(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('FORM_GETFIELDRECURSIVE_NO_FORM_INSTANCE'); - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ]); - $fieldset = new \codename\core\ui\fieldset([ - 'fieldset_id' => 'example', - 'fieldset_name' => 'example', - 'fieldset_name_override' => 'example', - ]); - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_value' => 'example', - ])); - $form->addFieldset($fieldset); - - $field = $form->getFieldRecursive([ 'example', 'example' ]); - } - - /** - * [testFormFieldsetWithoutFormInstance description] - */ - public function testFormFieldsetWithoutFormInstance(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('FORM_GETFIELDRECURSIVE_INVALID_FORM_INSTANCE'); - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ]); - $fieldset = new \codename\core\ui\fieldset([ - 'fieldset_id' => 'example', - 'fieldset_name' => 'example', - 'fieldset_name_override' => 'example', - ]); - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_value' => 'example', - 'form' => 'example', - ])); - $form->addFieldset($fieldset); - - $field = $form->getFieldRecursive([ 'example', 'example' ]); - } - - /** - * [testFormFieldsetWithFormInstance description] - */ - public function testFormFieldsetWithFormInstance(): void { - $form = new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ]); - $fieldset = new \codename\core\ui\fieldset([ - 'fieldset_id' => 'example', - 'fieldset_name' => 'example', - 'fieldset_name_override' => 'example', - ]); - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'example', - 'field_type' => 'text', - 'field_value' => 'example', - 'form' => (new \codename\core\ui\form([ - 'form_id' => 'exampleform', - 'form_action' => 'get', - 'form_method' => '', - ])), - ])); - $form->addFieldset($fieldset); - - $field = $form->getFieldRecursive([ 'example', 'example' ]); - $this->assertNull($field); - } - - /** - * [testJsonSerialize description] - */ - public function testJsonSerialize(): void { - $form = new \codename\core\ui\form([ - 'form_id' => 'serialized_form', - 'form_action' => '', - 'form_method' => '', - ]); - - $form->addField(new \codename\core\ui\field([ - 'field_name' => 'field1', - 'field_type' => 'input', - 'field_value' => 'value1', - ])); - - $fieldset = new \codename\core\ui\fieldset([ - 'fieldset_id' => 'serialized_fieldset', - 'fieldset_name' => 'serialized_fieldset_name', - ]); - $form->addFieldset($fieldset); - - $fieldset->addField(new \codename\core\ui\field([ - 'field_name' => 'field2', - 'field_type' => 'input', - 'field_value' => 'value2', - ])); - - - $jsonData = json_decode(json_encode($form), true); - $this->assertEquals('serialized_form', $jsonData['config']['form_id']); - $this->assertEquals('', $jsonData['config']['form_action']); - $this->assertEquals('', $jsonData['config']['form_method']); - $this->assertCount(1, $jsonData['fields']); - $this->assertEquals('field1', $jsonData['fields'][0]['field_name']); - $this->assertEquals('value1', $jsonData['fields'][0]['field_value']); - $this->assertCount(1, $jsonData['fieldsets']); - $this->assertCount(1, $jsonData['fieldsets'][0]['fields']); - $this->assertEquals('field2', $jsonData['fieldsets'][0]['fields'][0]['field_name']); - $this->assertEquals('value2', $jsonData['fieldsets'][0]['fields'][0]['field_value']); - } - + /** + * [testInvalidConstruct description] + * @throws ReflectionException + * @throws exception + */ + public function testInvalidConstruct(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_CONSTRUCT_CONFIGURATIONINVALID'); + + new form([ + 'form_id' => 'exampleform', + 'form_action' => 'post', + ]); + } + + /** + * [testEmptyForm description] + * @throws ReflectionException + * @throws exception + */ + public function testEmptyForm(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_OUTPUT_FORMISEMPTY'); + + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'post', + 'form_method' => '', + ]); + + $form->output(true); + } + + /** + * [testGeneric description] + * @throws ReflectionException + * @throws exception + */ + public function testGeneric(): void + { + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'post', + 'form_method' => '', + ]); + + static::assertEquals([ + 'form_id' => 'exampleform', + 'form_action' => 'post', + 'form_method' => '', + ], $form->config); + static::assertCount(0, $form->getFields()); + static::assertCount(0, $form->getFieldsets()); + static::assertEmpty($form->getErrorstack()->getErrors()); + + static::assertEmpty($form->getTemplateEngine()); + + $templateEngine = overrideableApp::getTemplateEngine(); + $form->setTemplateEngine($templateEngine); + static::assertNotEmpty($form->getTemplateEngine()); + } + + /** + * [testInvalidFormWithFields description] + * @throws ReflectionException + * @throws exception + */ + public function testInvalidFormWithFields(): void + { + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ]); + + $form->addField( + new field([ + 'field_name' => 'invalidexample', + 'field_type' => 'text', + 'field_value' => null, + 'field_required' => true, + ]) + ); + $form->addField( + new field([ + 'field_name' => 'noninputexample', + 'field_type' => 'text', + 'field_value' => null, + 'field_noninput' => true, + ]) + ); + $form->addField( + new field([ + 'field_name' => 'invalidexample2', + 'field_type' => 'text', + 'field_value' => 'invalidexample2', + ]), + 0 + ); + + $fields = $form->getFields(); + static::assertCount(3, $fields); + static::assertInstanceOf(field::class, $fields[0]); + static::assertInstanceOf(field::class, $fields[1]); + static::assertInstanceOf(field::class, $fields[2]); + + static::assertEquals([ + 'field_name' => 'invalidexample2', + 'field_type' => 'text', + 'field_id' => 'invalidexample2', + 'field_fieldtype' => 'input', + 'field_class' => 'input', + 'field_required' => false, + 'field_readonly' => false, + 'field_ajax' => false, + 'field_noninput' => false, + 'field_placeholder' => '', + 'field_value' => 'invalidexample2', + 'field_datatype' => 'text', + 'field_validator' => '', + 'field_description' => '', + 'field_title' => '', + ], $fields[0]->getConfig()->get()); + + $form->setId('invalidexampleform'); + $form->setAction('post'); + + static::assertEquals([ + 'form_id' => 'core_form_invalidexampleform', + 'form_action' => 'post', + 'form_method' => '', + ], $form->config); + static::assertCount(3, $form->getFields()); + static::assertCount(0, $form->getFieldsets()); + static::assertEmpty($form->getErrorstack()->getErrors()); + + static::assertFalse($form->isSent()); + + $result = $form->output(true); + static::assertNotEmpty($result); + + overrideableApp::getRequest()->setData('formSentcore_form_invalidexampleform', true); + + static::assertTrue($form->isSent()); + + static::assertFalse($form->isValid()); + + $errors = $form->getErrorstack()->getErrors(); + + static::assertCount(1, $errors); + static::assertEquals([ + [ + '__IDENTIFIER' => 'invalidexample', + '__CODE' => 'VALIDATION.FIELD_NOT_SET', + '__TYPE' => 'VALIDATION', + '__DETAILS' => null, + ], + ], $errors); + } + + /** + * [testValidFormWithFields description] + * @throws ReflectionException + * @throws exception + */ + public function testValidFormWithFields(): void + { + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ]); + + $form->addField( + new field([ + 'field_name' => 'validfieldexample', + 'field_type' => 'text', + 'field_value' => 'validfieldexample', + 'field_required' => true, + ]) + ); + $form->addField( + new field([ + 'field_name' => 'validfieldexample2', + 'field_type' => 'text', + 'field_value' => 'validfieldexample2', + ]), + 0 + ); + + $fields = $form->getFields(); + static::assertCount(2, $fields); + static::assertInstanceOf(field::class, $fields[0]); + static::assertInstanceOf(field::class, $fields[1]); + + static::assertEquals([ + 'field_name' => 'validfieldexample2', + 'field_type' => 'text', + 'field_id' => 'validfieldexample2', + 'field_fieldtype' => 'input', + 'field_class' => 'input', + 'field_required' => false, + 'field_readonly' => false, + 'field_ajax' => false, + 'field_noninput' => false, + 'field_placeholder' => '', + 'field_value' => 'validfieldexample2', + 'field_datatype' => 'text', + 'field_validator' => '', + 'field_description' => '', + 'field_title' => '', + ], $fields[0]->getConfig()->get()); + + $form->setId('validfieldexampleform'); + $form->setAction('post'); + + static::assertEquals([ + 'form_id' => 'core_form_validfieldexampleform', + 'form_action' => 'post', + 'form_method' => '', + ], $form->config); + static::assertCount(2, $form->getFields()); + static::assertCount(0, $form->getFieldsets()); + static::assertEmpty($form->getErrorstack()->getErrors()); + + static::assertFalse($form->isSent()); + + overrideableApp::getRequest()->setData('formSentcore_form_validfieldexampleform', true); + overrideableApp::getRequest()->setData('validfieldexample', 'validfieldexample'); + + static::assertTrue($form->isSent()); + static::assertTrue($form->isValid()); + + $errors = $form->getErrorstack()->getErrors(); + static::assertCount(0, $errors); + static::assertEmpty($errors); + + $data = $form->getData(); + $data = $form->normalizeData($data); + static::assertEquals([ + 'validfieldexample' => 'validfieldexample', + ], $data); + } + + /** + * [testInvalidFormWithFields description] + * @throws ReflectionException + * @throws exception + */ + public function testValidFormWithFieldset(): void + { + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ]); + + $fieldset = new fieldset([ + 'fieldset_id' => 'example', + 'fieldset_name' => 'example', + 'fieldset_name_override' => 'example', + ]); + + $fieldset->addField( + new field([ + 'field_name' => 'validexample', + 'field_type' => 'text', + 'field_value' => null, + 'field_required' => true, + ]) + ); + $fieldset->addField( + new field([ + 'field_name' => 'validexample2', + 'field_type' => 'text', + 'field_value' => 'validexample2', + ]), + 0 + ); + + $form->addFieldset($fieldset); + + $fieldsets = $form->getFieldsets(); + static::assertCount(1, $fieldsets); + static::assertInstanceOf(fieldset::class, $fieldsets[0]); + + $form->setId('validexampleform'); + $form->setAction('post'); + + static::assertEquals([ + 'form_id' => 'core_form_validexampleform', + 'form_action' => 'post', + 'form_method' => '', + ], $form->config); + static::assertCount(0, $form->getFields()); + static::assertCount(1, $form->getFieldsets()); + static::assertEmpty($form->getErrorstack()->getErrors()); + + static::assertFalse($form->isSent()); + + overrideableApp::getRequest()->setData('formSentcore_form_validexampleform', true); + overrideableApp::getRequest()->setData('validexample', 'validexample'); + + static::assertTrue($form->isSent()); + static::assertTrue($form->isValid()); + + $errors = $form->getErrorstack()->getErrors(); + static::assertCount(0, $errors); + static::assertEmpty($errors); + + $data = $form->getData(); + // NOTE: returned null, if not fields is set + $data = $form->normalizeData($data); + static::assertEmpty($data); + } + + /** + * [testFormOutput description] + * @throws ReflectionException + * @throws exception + */ + public function testFormOutput(): void + { + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ]); + + $form->addField( + new field([ + 'field_name' => 'fieldexample', + 'field_type' => 'text', + 'field_value' => 'fieldexample', + ]) + ); + $form->addField( + new field([ + 'field_name' => 'fieldexample2', + 'field_type' => 'text', + 'field_value' => 'fieldexample2', + ]) + ); + + $fieldset = new fieldset([ + 'fieldset_id' => 'example', + 'fieldset_name' => 'example', + 'fieldset_name_override' => 'example', + ]); + $fieldset->addField( + new field([ + 'field_name' => 'fieldsetexample', + 'field_type' => 'text', + 'field_value' => 'fieldsetexample', + ]) + ); + $fieldset->addField( + new field([ + 'field_name' => 'fieldsetexample2', + 'field_type' => 'text', + 'field_value' => 'fieldsetexample2', + ]), + 0 + ); + $form->addFieldset($fieldset); + + $fieldset->setType('default'); + + $output = $form->output(); + static::assertEquals('frontend/form/default/form', $output); + } + + /** + * [testFormSearchFieldByGetField description] + * @throws ReflectionException + * @throws exception + */ + public function testFormSearchFieldByGetField(): void + { + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ]); + $form->addField( + new field([ + 'field_name' => 'validfieldexample', + 'field_type' => 'text', + 'field_value' => 'validfieldexample', + 'field_required' => true, + ]) + ); + $form->addField( + new field([ + 'field_name' => 'validfieldexample2', + 'field_type' => 'text', + 'field_value' => 'validfieldexample2', + ]), + 0 + ); + $fieldset = new fieldset([ + 'fieldset_id' => 'example', + 'fieldset_name' => 'example', + 'fieldset_name_override' => 'example', + ]); + $fieldset->addField( + new field([ + 'field_name' => 'validexample', + 'field_type' => 'text', + 'field_value' => null, + 'field_required' => true, + ]) + ); + $fieldset->addField( + new field([ + 'field_name' => 'validexample2', + 'field_type' => 'text', + 'field_value' => 'validexample2', + ]), + 0 + ); + $form->addFieldset($fieldset); + + $field = $form->getField('validfieldexample2'); + static::assertEquals([ + 'field_name' => 'validfieldexample2', + 'field_type' => 'text', + 'field_id' => 'validfieldexample2', + 'field_fieldtype' => 'input', + 'field_class' => 'input', + 'field_required' => false, + 'field_readonly' => false, + 'field_ajax' => false, + 'field_noninput' => false, + 'field_placeholder' => '', + 'field_value' => 'validfieldexample2', + 'field_datatype' => 'text', + 'field_validator' => '', + 'field_description' => '', + 'field_title' => '', + ], $field->getConfig()->get()); + + $field = $form->getField('validexample2'); + static::assertEquals([ + 'field_name' => 'validexample2', + 'field_type' => 'text', + 'field_id' => 'validexample2', + 'field_fieldtype' => 'input', + 'field_class' => 'input', + 'field_required' => false, + 'field_readonly' => false, + 'field_ajax' => false, + 'field_noninput' => false, + 'field_placeholder' => '', + 'field_value' => 'validexample2', + 'field_datatype' => 'text', + 'field_validator' => '', + 'field_description' => '', + 'field_title' => '', + ], $field->getConfig()->get()); + + $field = $form->getField('fieldnotfound'); + static::assertNull($field); + } + + /** + * [testFormSearchFieldByGetFieldRecursive description] + * @throws ReflectionException + * @throws exception + */ + public function testFormSearchFieldByGetFieldRecursive(): void + { + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ]); + $form->addField( + new field([ + 'field_name' => 'validfieldexample', + 'field_type' => 'text', + 'field_value' => 'validfieldexample', + 'field_required' => true, + ]) + ); + $form->addField( + new field([ + 'field_name' => 'validfieldexample2', + 'field_type' => 'text', + 'field_value' => 'validfieldexample2', + ]), + 0 + ); + $fieldset = new fieldset([ + 'fieldset_id' => 'example', + 'fieldset_name' => 'example', + 'fieldset_name_override' => 'example', + ]); + $fieldset->addField( + new field([ + 'field_name' => 'validexample', + 'field_type' => 'text', + 'field_value' => null, + 'field_required' => true, + ]) + ); + $fieldset->addField( + new field([ + 'field_name' => 'validexample2', + 'field_type' => 'text', + 'field_value' => 'validexample2', + ]), + 0 + ); + $form->addFieldset($fieldset); + + $field = $form->getFieldRecursive(['validfieldexample2']); + static::assertEquals([ + 'field_name' => 'validfieldexample2', + 'field_type' => 'text', + 'field_id' => 'validfieldexample2', + 'field_fieldtype' => 'input', + 'field_class' => 'input', + 'field_required' => false, + 'field_readonly' => false, + 'field_ajax' => false, + 'field_noninput' => false, + 'field_placeholder' => '', + 'field_value' => 'validfieldexample2', + 'field_datatype' => 'text', + 'field_validator' => '', + 'field_description' => '', + 'field_title' => '', + ], $field->getConfig()->get()); + + $field = $form->getFieldRecursive(['validexample2']); + static::assertEquals([ + 'field_name' => 'validexample2', + 'field_type' => 'text', + 'field_id' => 'validexample2', + 'field_fieldtype' => 'input', + 'field_class' => 'input', + 'field_required' => false, + 'field_readonly' => false, + 'field_ajax' => false, + 'field_noninput' => false, + 'field_placeholder' => '', + 'field_value' => 'validexample2', + 'field_datatype' => 'text', + 'field_validator' => '', + 'field_description' => '', + 'field_title' => '', + ], $field->getConfig()->get()); + + $field = $form->getFieldRecursive(['fieldnotfound']); + static::assertNull($field); + } + + /** + * [testFormFieldWithoutForm description] + * @throws ReflectionException + * @throws exception + */ + public function testFormFieldWithoutForm(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('FORM_GETFIELDRECURSIVE_NO_FORM_INSTANCE'); + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ]); + $form->addField( + new field([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_value' => 'example', + ]) + ); + + $form->getFieldRecursive(['example', 'example']); + } + + /** + * [testFormFieldWithoutFormInstance description] + * @throws ReflectionException + * @throws exception + */ + public function testFormFieldWithoutFormInstance(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('FORM_GETFIELDRECURSIVE_INVALID_FORM_INSTANCE'); + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ]); + $form->addField( + new field([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_value' => 'example', + 'form' => 'example', + ]) + ); + + $form->getFieldRecursive(['example', 'example']); + } + + /** + * [testFormFieldWithFormInstance description] + * @throws ReflectionException + * @throws exception + */ + public function testFormFieldWithFormInstance(): void + { + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ]); + $form->addField( + new field([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_value' => 'example', + 'form' => (new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ])), + ]) + ); + + $field = $form->getFieldRecursive(['example', 'example']); + static::assertNull($field); + } + + /** + * [testFormFieldsetWithoutForm description] + * @throws ReflectionException + * @throws exception + */ + public function testFormFieldsetWithoutForm(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('FORM_GETFIELDRECURSIVE_NO_FORM_INSTANCE'); + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ]); + $fieldset = new fieldset([ + 'fieldset_id' => 'example', + 'fieldset_name' => 'example', + 'fieldset_name_override' => 'example', + ]); + $fieldset->addField( + new field([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_value' => 'example', + ]) + ); + $form->addFieldset($fieldset); + + $form->getFieldRecursive(['example', 'example']); + } + + /** + * [testFormFieldsetWithoutFormInstance description] + * @throws ReflectionException + * @throws exception + */ + public function testFormFieldsetWithoutFormInstance(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('FORM_GETFIELDRECURSIVE_INVALID_FORM_INSTANCE'); + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ]); + $fieldset = new fieldset([ + 'fieldset_id' => 'example', + 'fieldset_name' => 'example', + 'fieldset_name_override' => 'example', + ]); + $fieldset->addField( + new field([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_value' => 'example', + 'form' => 'example', + ]) + ); + $form->addFieldset($fieldset); + + $form->getFieldRecursive(['example', 'example']); + } + + /** + * [testFormFieldsetWithFormInstance description] + * @throws ReflectionException + * @throws exception + */ + public function testFormFieldsetWithFormInstance(): void + { + $form = new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ]); + $fieldset = new fieldset([ + 'fieldset_id' => 'example', + 'fieldset_name' => 'example', + 'fieldset_name_override' => 'example', + ]); + $fieldset->addField( + new field([ + 'field_name' => 'example', + 'field_type' => 'text', + 'field_value' => 'example', + 'form' => (new form([ + 'form_id' => 'exampleform', + 'form_action' => 'get', + 'form_method' => '', + ])), + ]) + ); + $form->addFieldset($fieldset); + + $field = $form->getFieldRecursive(['example', 'example']); + static::assertNull($field); + } + + /** + * [testJsonSerialize description] + * @throws ReflectionException + * @throws exception + */ + public function testJsonSerialize(): void + { + $form = new form([ + 'form_id' => 'serialized_form', + 'form_action' => '', + 'form_method' => '', + ]); + + $form->addField( + new field([ + 'field_name' => 'field1', + 'field_type' => 'input', + 'field_value' => 'value1', + ]) + ); + + $fieldset = new fieldset([ + 'fieldset_id' => 'serialized_fieldset', + 'fieldset_name' => 'serialized_fieldset_name', + ]); + $form->addFieldset($fieldset); + + $fieldset->addField( + new field([ + 'field_name' => 'field2', + 'field_type' => 'input', + 'field_value' => 'value2', + ]) + ); + + + $jsonData = json_decode(json_encode($form), true); + static::assertEquals('serialized_form', $jsonData['config']['form_id']); + static::assertEquals('', $jsonData['config']['form_action']); + static::assertEquals('', $jsonData['config']['form_method']); + static::assertCount(1, $jsonData['fields']); + static::assertEquals('field1', $jsonData['fields'][0]['field_name']); + static::assertEquals('value1', $jsonData['fields'][0]['field_value']); + static::assertCount(1, $jsonData['fieldsets']); + static::assertCount(1, $jsonData['fieldsets'][0]['fields']); + static::assertEquals('field2', $jsonData['fieldsets'][0]['fields'][0]['field_name']); + static::assertEquals('value2', $jsonData['fieldsets'][0]['fields'][0]['field_value']); + } + + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws ErrorException + * @throws Throwable + * @throws compileErrorException + * @throws coreErrorException + * @throws coreWarningException + * @throws parseException + * @throws recoverableErrorException + * @throws strictException + * @throws userErrorException + * @throws userWarningException + * @throws warningException + * @throws exception + */ + protected function setUp(): void + { + parent::setUp(); + + $app = static::createApp(); + overrideableApp::__injectApp([ + 'vendor' => 'codename', + 'app' => 'core-ui', + 'namespace' => '\\codename\\core\\ui', + ]); + + $app::getAppstack(); + + static::setEnvironmentConfig([ + 'test' => [ + 'cache' => [ + 'default' => [ + 'driver' => 'memory', + ], + ], + 'translate' => [ + 'default' => [ + 'driver' => 'json', + 'inherit' => true, + ], + ], + 'templateengine' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + ], + ]); + } } diff --git a/tests/frontend/element/tableTest.php b/tests/frontend/element/tableTest.php index eb2bb44..61d3c36 100644 --- a/tests/frontend/element/tableTest.php +++ b/tests/frontend/element/tableTest.php @@ -1,120 +1,167 @@ 'codename', - 'app' => 'core-ui', - 'namespace' => '\\codename\\core\\ui' - ]); - - $app->getAppstack(); - - static::setEnvironmentConfig([ - 'test' => [ - 'cache' => [ - 'default' => [ - 'driver' => 'memory' - ] - ], - 'translate' => [ - 'default' => [ - 'driver' => 'json', - 'inherit' => true, - ] - ], - 'templateengine' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - ] - ]); - } - - /** - * [testInvalidConstruct description] - */ - public function testInvalidConstruct(): void { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_CORE_UI_FRONTEND_ELEMENT_INVALID_CONFIG'); - $form = new core_ui_frontend_element_table([], []); - } - - /** - * [testOutputDataWithoutData description] - */ - public function testOutputDataWithoutData(): void { - $table = new \codename\core\ui\frontend\element\table([ - 'columns' => [ 0, 1, 2 ] - ]); - - $outputData = $table->outputData(); - $this->assertEquals([ - 'max' => [ 1, 1, 1 ], - 'header' => [ 0, 1, 2 ], - 'rows' => [], - 'footer' => [] - ], $outputData); - - } - - /** - * [testOutputDataWithoutData description] - */ - public function testOutputDataWithData(): void { - $data = [ - [ - 0 => 'Test 1', - 1 => 'Test 2', - 2 => 3, - ] - ]; - $table = new \codename\core\ui\frontend\element\table([], $data); - - $outputData = $table->outputData(); - $this->assertEquals([ - 'max' => [ 6, 6, 1 ], - 'header' => [ 0, 1, 2 ], - 'rows' => [ - [ 'Test 1', 'Test 2', 3 ] - ], - 'footer' => [] - ], $outputData, json_encode($outputData)); - - } - - /** - * [testOutputStringWithoutData description] - */ - public function testOutputStringWithoutData(): void { - $table = new \codename\core\ui\frontend\element\table(); - $this->assertEquals('frontend/element/table/default', $table->outputString()); - } - + /** + * [testInvalidConstruct description] + * @throws ReflectionException + * @throws exception + */ + public function testInvalidConstruct(): void + { + $this->expectException(exception::class); + $this->expectExceptionMessage('EXCEPTION_CORE_UI_FRONTEND_ELEMENT_INVALID_CONFIG'); + new core_ui_frontend_element_table([], []); + } + + /** + * [testOutputDataWithoutData description] + * @throws ReflectionException + * @throws exception + */ + public function testOutputDataWithoutData(): void + { + $table = new table([ + 'columns' => [0, 1, 2], + ]); + + $outputData = $table->outputData(); + static::assertEquals([ + 'max' => [1, 1, 1], + 'header' => [0, 1, 2], + 'rows' => [], + 'footer' => [], + ], $outputData); + } + + /** + * [testOutputDataWithoutData description] + * @throws ReflectionException + * @throws exception + */ + public function testOutputDataWithData(): void + { + $data = [ + [ + 0 => 'Test 1', + 1 => 'Test 2', + 2 => 3, + ], + ]; + $table = new table([], $data); + + $outputData = $table->outputData(); + static::assertEquals([ + 'max' => [6, 6, 1], + 'header' => [0, 1, 2], + 'rows' => [ + ['Test 1', 'Test 2', 3], + ], + 'footer' => [], + ], $outputData, json_encode($outputData)); + } + + /** + * [testOutputStringWithoutData description] + * @throws ReflectionException + * @throws exception + */ + public function testOutputStringWithoutData(): void + { + $table = new table(); + static::assertEquals('frontend/element/table/default', $table->outputString()); + } + + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws ErrorException + * @throws Throwable + * @throws compileErrorException + * @throws coreErrorException + * @throws coreWarningException + * @throws parseException + * @throws recoverableErrorException + * @throws strictException + * @throws userErrorException + * @throws userWarningException + * @throws warningException + * @throws exception + */ + protected function setUp(): void + { + parent::setUp(); + + $app = static::createApp(); + overrideableApp::__injectApp([ + 'vendor' => 'codename', + 'app' => 'core-ui', + 'namespace' => '\\codename\\core\\ui', + ]); + + $app::getAppstack(); + + static::setEnvironmentConfig([ + 'test' => [ + 'cache' => [ + 'default' => [ + 'driver' => 'memory', + ], + ], + 'translate' => [ + 'default' => [ + 'driver' => 'json', + 'inherit' => true, + ], + ], + 'templateengine' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + ], + ]); + } } /** * [core_ui_frontend_element_table description] */ -class core_ui_frontend_element_table extends \codename\core\ui\frontend\element\table { - - protected $configValidatorName = 'number'; - - function __construct(array $config = array(), array $data = array()) { - parent::__construct($config, $data); - } +class core_ui_frontend_element_table extends table +{ + /** + * validator used for validating the given configuration + * @var string + */ + protected $configValidatorName = 'number'; + + /** + * @param array $config + * @param array $data + * @throws ReflectionException + * @throws exception + */ + public function __construct(array $config = [], array $data = []) + { + parent::__construct($config, $data); + } } diff --git a/tests/templateengine/twig/twigTest.php b/tests/templateengine/twig/twigTest.php index e9829f1..75c911f 100644 --- a/tests/templateengine/twig/twigTest.php +++ b/tests/templateengine/twig/twigTest.php @@ -1,394 +1,536 @@ __setApp('twigtest'); - $app->__setVendor('codename'); - $app->__setNamespace('\\codename\\core\\ui\\tests\\templateengine\\twig'); - $app->__setHomedir(__DIR__); - - $app->getAppstack(); - - // NOTE: if we reset the app in setUp(), we have to execute this initialization routine again, no matter what. - // avoid re-init - // if(static::$initialized) { - // return; - // } - - static::$initialized = true; - - static::setEnvironmentConfig([ - 'test' => [ - 'filesystem' =>[ - 'local' => [ - 'driver' => 'local', - ] - ], - 'translate' => [ - 'default' => [ - 'driver' => 'dummy', - ] - ], - 'templateengine' => [ - 'default' => [ - 'driver' => 'twig', - ], - 'sandbox_available' => [ - 'driver' => 'twig', - 'sandbox_enabled' => true, - 'sandbox_mode' => null // not globally enabled - ], - 'sandbox_global' => [ - 'driver' => 'twig', - 'sandbox_enabled' => true, - 'sandbox_mode' => 'global' // sandbox for everything + /** + * Makes sure we get the correct class + * and implicitly tests the basic initialization + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testInstance(): void + { + static::assertInstanceOf(twig::class, app::getTemplateEngine()); + } + + /** + * Basic test for getting the internal client name + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testGetClientName(): void + { + $instance = app::getTemplateEngine(); + if ($instance instanceof clientInterface) { + static::assertEquals('templateenginedefault', $instance->getClientName('default')); + } + } + + /** + * Test that renaming the client fails (after it has been initialized via app/env) + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testRenameClientInstanceFails(): void + { + $instance = app::getTemplateEngine(); + $this->expectException(\codename\core\exception::class); + $this->expectExceptionMessage('EXCEPTION_CORE_CLIENT_INTERFACE_CANNOT_RENAME_CLIENT'); + if ($instance instanceof clientInterface) { + $instance->setClientName('abc'); + } + } + + /** + * Makes sure renderStringSandboxed fails, if sandbox is not enabled at all + * @throws LoaderError + * @throws ReflectionException + * @throws SyntaxError + * @throws Throwable + * @throws \codename\core\exception + */ + public function testRenderStringSandboxedFailsNotEnabled(): void + { + $instance = app::getTemplateEngine(); + if ($instance instanceof twig) { + $this->expectException(\codename\core\exception::class); + $this->expectExceptionMessage('TEMPLATEENGINE_TWIG_NO_SANDBOX_INSTANCE'); + $instance->renderStringSandboxed('{{ someVariable }}', []); + } + } + + /** + * Makes sure renderSandboxed fails if sandbox is not enabled at all + * @throws LoaderError + * @throws ReflectionException + * @throws SyntaxError + * @throws Throwable + * @throws RuntimeError + * @throws \codename\core\exception + */ + public function testRenderSandboxedFailsNotEnabled(): void + { + $instance = app::getTemplateEngine(); + if ($instance instanceof twig) { + $this->expectException(\codename\core\exception::class); + $this->expectExceptionMessage('TEMPLATEENGINE_TWIG_NO_SANDBOX_INSTANCE'); + $instance->renderSandboxed('test_templates/example1', []); + } + } + + /** + * Overriding the sandbox + * @throws LoaderError + * @throws ReflectionException + * @throws SyntaxError + * @throws Throwable + * @throws \codename\core\exception + */ + public function testSandboxOverride(): void + { + $instance = new twig([ + 'sandbox_enabled' => false, + 'sandbox_mode' => 'override', + ]); + app::getResponse()->setData('key', 'yes'); + $rendered = $instance->renderStringSandboxed('{{ sandboxedVariable }} {{ response.getData("key") }}', ['sandboxedVariable' => 'foo']); + static::assertEquals("foo yes", $rendered); + } + + /** + * [testStringTemplate description] + * @throws LoaderError + * @throws ReflectionException + * @throws SyntaxError + * @throws Throwable + * @throws \codename\core\exception + */ + public function testStringTemplate(): void + { + $instance = app::getTemplateEngine('sandbox_available'); + if ($instance instanceof twig) { + $rendered = $instance->renderStringSandboxed('{{ someVariable }}', ['someVariable' => '123']); + static::assertEquals('123', $rendered); + } + } + + /** + * [testStringTemplateSandboxedGlobal description] + * @throws LoaderError + * @throws ReflectionException + * @throws SyntaxError + * @throws Throwable + * @throws \codename\core\exception + */ + public function testStringTemplateSandboxedGlobal(): void + { + $instance = app::getTemplateEngine('sandbox_global'); + if ($instance instanceof twig) { + $rendered = $instance->renderStringSandboxed('{{ someVariable }}', ['someVariable' => '123']); + static::assertEquals('123', $rendered); + + $this->expectException(SecurityError::class); + app::getResponse()->setData('key', 'abc'); + $instance->renderStringSandboxed('{{ response.getData("key") }}', []); + } + } + + /** + * [testStringTemplateSandboxedStandby description] + * @throws LoaderError + * @throws ReflectionException + * @throws SyntaxError + * @throws Throwable + * @throws \codename\core\exception + */ + public function testStringTemplateSandboxedStandby(): void + { + $instance = app::getTemplateEngine('sandbox_available'); + if ($instance instanceof twig) { + $rendered = $instance->renderStringSandboxed('{{ someVariable }}', ['someVariable' => '123']); + static::assertEquals('123', $rendered); + + $this->expectException(SecurityError::class); + app::getResponse()->setData('key', 'abc'); + $instance->renderStringSandboxed('{{ response.getData("key") }}', []); + } + } + + /** + * Simple twig file + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testSimpleFileTemplate(): void + { + $rendered = app::getTemplateEngine()->render('test_templates/example1', ['example1' => 'abc']); + static::assertEquals("Example1 abc\n", $rendered); + } + + /** + * [testDefectiveTemplateSyntaxError description] + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testDefectiveTemplateSyntaxError(): void + { + $this->expectException(SyntaxError::class); + app::getTemplateEngine()->render('test_templates/test_defective_syntax_error', []); + } + + /** + * [testDefectiveTemplateRuntimeError description] + * @throws LoaderError + * @throws ReflectionException + * @throws RuntimeError + * @throws SyntaxError + * @throws Throwable + * @throws \codename\core\exception + */ + public function testDefectiveTemplateRuntimeError(): void + { + $this->expectExceptionMessage('Crash'); + $instance = new twig([]); + $instance->addFunction('crashingFunction', function () { + throw new Exception('Crash'); + }); + $instance->render('test_templates/test_defective_crashingFunction', []); + } + + /** + * Tests config with a differing file extension + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testOtherTemplateFileExtension(): void + { + $rendered = app::getTemplateEngine('otherext')->render('test_templates/test', ['someVariable' => 'yes']); + static::assertEquals("TestOtherExt yes\n", $rendered); + } + + /** + * Tests config with a differing file extension, looking for a nonexisting file + * (implicitly) + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testOtherTemplateFileExtensionNonexistingWillFail(): void + { + $this->expectException(LoaderError::class); + // NOTE: example1.twig DOES exist, but example1.otherext.twig does NOT. + app::getTemplateEngine('otherext')->render('test_templates/example1', ['someVariable' => 'yes']); + } + + /** + * Tests rendering a template file + * in a sandbox - without accessing disallowed stuff + * @throws LoaderError + * @throws ReflectionException + * @throws RuntimeError + * @throws SyntaxError + * @throws Throwable + * @throws \codename\core\exception + */ + public function testRenderSandboxed(): void + { + $instance = app::getTemplateEngine('sandbox_available'); + if (!($instance instanceof twig)) { + static::fail('setup fail'); + } + $rendered = $instance->renderSandboxed('test_templates/test_sandboxed', ['sandboxedVariable' => 'foo']); + static::assertEquals("foo\n", $rendered); + } + + /** + * Tests accessing a disallowed method + * in a template file will fail + * @throws LoaderError + * @throws ReflectionException + * @throws RuntimeError + * @throws SyntaxError + * @throws Throwable + * @throws \codename\core\exception + */ + public function testRenderSandboxedWillFail(): void + { + $this->expectException(SecurityError::class); + app::getRequest(); + app::getResponse()->setData('key', 'abc'); + $instance = app::getTemplateEngine('sandbox_global'); + if (!($instance instanceof twig)) { + static::fail('setup fail'); + } + $instance->renderSandboxed('test_templates/test_sandboxed_access', ['sandboxedVariable' => 'foo']); + } + + /** + * Tests accessing allowed method access + * in a template file will succeed + * @throws LoaderError + * @throws ReflectionException + * @throws RuntimeError + * @throws SyntaxError + * @throws Throwable + * @throws \codename\core\exception + */ + public function testRenderSandboxedWillSucceed(): void + { + app::getRequest(); + app::getResponse()->setData('key', 'abc'); + + $instance = new twig([ + 'sandbox_enabled' => true, + 'sandbox' => [ + 'methods' => [ + response::class => 'getData', + ], ], - 'otherext' => [ - 'driver' => 'twig', - 'template_file_extension' => '.otherext.twig' - ] - ], - 'log' => [ - 'default' => [ - 'driver' => 'system', - 'data' => [ - 'name' => 'dummy' - ] - ] - ], - ] - ]); - } - - /** - * Makes sure we get the correct class - * and implicitly tests the basic initialization - */ - public function testInstance(): void { - $this->assertInstanceOf(\codename\core\ui\templateengine\twig::class, app::getTemplateEngine()); - } - - /** - * Basic test for getting the internal client name - */ - public function testGetClientName(): void { - $instance = app::getTemplateEngine(); - if($instance instanceof \codename\core\clientInterface) { - $this->assertEquals('templateenginedefault', $instance->getClientName('default')); + ]); + + $rendered = $instance->renderSandboxed('test_templates/test_sandboxed_access', ['sandboxedVariable' => 'foo']); + static::assertEquals("foo abc\n", $rendered); } - } - - /** - * Test that renaming the client fails (after it has been initialized via app/env) - */ - public function testRenameClientInstanceFails(): void { - $instance = app::getTemplateEngine(); - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('EXCEPTION_CORE_CLIENT_INTERFACE_CANNOT_RENAME_CLIENT'); - if($instance instanceof \codename\core\clientInterface) { - $instance->setClientName('abc'); + + /** + * Tests accessing allowed method access + * in a sandboxed string template succeed + * @throws LoaderError + * @throws ReflectionException + * @throws SyntaxError + * @throws Throwable + * @throws \codename\core\exception + */ + public function testRenderSandboxedStringWillSucceed(): void + { + app::getRequest(); + app::getResponse()->setData('key', 'xyz'); + + $instance = new twig([ + 'sandbox_enabled' => true, + 'sandbox' => [ + 'methods' => [ + response::class => 'getData', + ], + ], + ]); + + $rendered = $instance->renderStringSandboxed('{{ sandboxedVariable }} {{ response.getData("key") }}', ['sandboxedVariable' => 'foo']); + static::assertEquals("foo xyz", $rendered); } - } - - /** - * Makes sure renderStringSandboxed fails, if sandbox is not enabled at all - */ - public function testRenderStringSandboxedFailsNotEnabled(): void { - $instance = app::getTemplateEngine(); - if($instance instanceof \codename\core\ui\templateengine\twig) { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('TEMPLATEENGINE_TWIG_NO_SANDBOX_INSTANCE'); - $rendered = $instance->renderStringSandboxed('{{ someVariable }}', []); + + /** + * Tests a simple file inclusion, semi-absolute (relative to project FE root) + * but inherited across apps + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testIncludedFileTemplate(): void + { + $rendered = app::getTemplateEngine()->render('test_templates/example_includes', ['example1' => 'abc']); + static::assertEquals("Example1 abc\n", $rendered); } - } - - /** - * Makes sure renderSandboxed fails, if sandbox is not enabled at all - */ - public function testRenderSandboxedFailsNotEnabled(): void { - $instance = app::getTemplateEngine(); - if($instance instanceof \codename\core\ui\templateengine\twig) { - $this->expectException(\codename\core\exception::class); - $this->expectExceptionMessage('TEMPLATEENGINE_TWIG_NO_SANDBOX_INSTANCE'); - $rendered = $instance->renderSandboxed('test_templates/example1', []); + + /** + * Special core-ui feature: relative paths may be used in a twig-template + * (relative to current file/cwd) + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testIncludedRelativeFileTemplate(): void + { + $rendered = app::getTemplateEngine()->render('test_templates/example_includes_relative', ['example1' => 'abc']); + static::assertEquals("Example1 abc\n", $rendered); } - } - - /** - * Overriding the sandbox - */ - public function testSandboxOverride(): void { - $instance = new \codename\core\ui\templateengine\twig([ - 'sandbox_enabled' => false, - 'sandbox_mode' => 'override', - ]); - app::getResponse()->setData('key', 'yes'); - $rendered = $instance->renderStringSandboxed('{{ sandboxedVariable }} {{ response.getData("key") }}', [ 'sandboxedVariable' => 'foo' ]); - $this->assertEquals("foo yes", $rendered); - } - - /** - * [testStringTemplate description] - */ - public function testStringTemplate(): void { - $instance = app::getTemplateEngine('sandbox_available'); - if($instance instanceof \codename\core\ui\templateengine\twig) { - $rendered = $instance->renderStringSandboxed('{{ someVariable }}', [ 'someVariable' => '123' ]); - $this->assertEquals('123', $rendered); + + /** + * Tests dynamic (non-sandboxed) template inclusion + * using full context passthrough + * @throws ReflectionException + * @throws \codename\core\exception + */ + public function testIncludeTemplateFromString(): void + { + $rendered = app::getTemplateEngine()->render('test_templates/test_include_template_from_string', [ + 'template' => 'TemplateRendered: {{ data.someVariable }}', + 'someVariable' => 'def123', + ]); + static::assertEquals("TemplateRendered: def123\n", $rendered); } - } - - /** - * [testStringTemplateSandboxedGlobal description] - */ - public function testStringTemplateSandboxedGlobal(): void { - $instance = app::getTemplateEngine('sandbox_global'); - if($instance instanceof \codename\core\ui\templateengine\twig) { - $rendered = $instance->renderStringSandboxed('{{ someVariable }}', [ 'someVariable' => '123' ]); - $this->assertEquals('123', $rendered); - - $this->expectException(\Twig\Sandbox\SecurityError::class); - app::getResponse()->setData('key', 'abc'); - $rendered = $instance->renderStringSandboxed('{{ response.getData("key") }}', []); + + /** + * Tests some core-ui provided Twig-"tests" + * (usage with varname is <...>) + * @throws LoaderError + * @throws ReflectionException + * @throws SyntaxError + * @throws Throwable + * @throws \codename\core\exception + */ + public function testIntegratedTwigTests(): void + { + $instance = app::getTemplateEngine('sandbox_available'); + if (!($instance instanceof twig)) { + static::fail('setup fail'); + } + // + // Tests 'array' test + // + $rendered = $instance->renderStringSandboxed('{{ value is array ? 1 : 0 }}', ['value' => [1, 2, 3]]); + static::assertEquals("1", $rendered); + $rendered = $instance->renderStringSandboxed('{{ value is array ? 1 : 0 }}', ['value' => 'abc']); + static::assertEquals("0", $rendered); + + // + // Tests 'string' test + // + $rendered = $instance->renderStringSandboxed('{{ value is string ? 1 : 0 }}', ['value' => 123]); + static::assertEquals("0", $rendered); + $rendered = $instance->renderStringSandboxed('{{ value is string ? 1 : 0 }}', ['value' => [1, 2, 3]]); + static::assertEquals("0", $rendered); + $rendered = $instance->renderStringSandboxed('{{ value is string ? 1 : 0 }}', ['value' => '123']); + static::assertEquals("1", $rendered); } - } - - /** - * [testStringTemplateSandboxedStandby description] - */ - public function testStringTemplateSandboxedStandby(): void { - $instance = app::getTemplateEngine('sandbox_available'); - if($instance instanceof \codename\core\ui\templateengine\twig) { - $rendered = $instance->renderStringSandboxed('{{ someVariable }}', [ 'someVariable' => '123' ]); - $this->assertEquals('123', $rendered); - - $this->expectException(\Twig\Sandbox\SecurityError::class); - app::getResponse()->setData('key', 'abc'); - $rendered = $instance->renderStringSandboxed('{{ response.getData("key") }}', []); + + /** + * [testRenderView description] + */ + public function testRenderView(): void + { + static::markTestIncomplete('renderView makes only sense when used in conjunction with app lifecycle'); } - } - - /** - * Simple twig file - */ - public function testSimpleFileTemplate(): void { - $rendered = app::getTemplateEngine()->render('test_templates/example1', [ 'example1' => 'abc' ]); - $this->assertEquals("Example1 abc\n", $rendered); - } - - /** - * [testDefectiveTemplateSyntaxError description] - */ - public function testDefectiveTemplateSyntaxError(): void { - $this->expectException(\Twig\Error\SyntaxError::class); - $rendered = app::getTemplateEngine()->render('test_templates/test_defective_syntax_error', []); - } - - /** - * [testDefectiveTemplateRuntimeError description] - */ - public function testDefectiveTemplateRuntimeError(): void { - $this->expectExceptionMessage('Crash'); - $instance = new \codename\core\ui\templateengine\twig([]); - $instance->addFunction('crashingFunction', function() { - throw new \Exception('Crash'); - }); - $rendered = $instance->render('test_templates/test_defective_crashingFunction', []); - } - - /** - * Tests config with a differing file extension - */ - public function testOtherTemplateFileExtension(): void { - $rendered = app::getTemplateEngine('otherext')->render('test_templates/test', [ 'someVariable' => 'yes' ]); - $this->assertEquals("TestOtherExt yes\n", $rendered); - } - - /** - * Tests config with a differing file extension, looking for a nonexisting file - * (implicitly) - */ - public function testOtherTemplateFileExtensionNonexistingWillFail(): void { - $this->expectException(\Twig\Error\LoaderError::class); - // NOTE: example1.twig DOES exist, but example1.otherext.twig does NOT. - $rendered = app::getTemplateEngine('otherext')->render('test_templates/example1', [ 'someVariable' => 'yes' ]); - } - - /** - * Tests rendering a template file - * in a sandbox - without accessing disallowed stuff - */ - public function testRenderSandboxed(): void { - $rendered = app::getTemplateEngine('sandbox_available')->renderSandboxed('test_templates/test_sandboxed', [ 'sandboxedVariable' => 'foo' ]); - $this->assertEquals("foo\n", $rendered); - } - - /** - * Tests accessing a disallowed method - * in a template file will fail - */ - public function testRenderSandboxedWillFail(): void { - $this->expectException(\Twig\Sandbox\SecurityError::class); - app::getRequest(); - app::getResponse()->setData('key', 'abc'); - $rendered = app::getTemplateEngine('sandbox_global')->renderSandboxed('test_templates/test_sandboxed_access', [ 'sandboxedVariable' => 'foo' ]); - } - - /** - * Tests accessing an allowed method access - * in a template file will succeed - */ - public function testRenderSandboxedWillSucceed(): void { - app::getRequest(); - app::getResponse()->setData('key', 'abc'); - - $instance = new \codename\core\ui\templateengine\twig([ - 'sandbox_enabled' => true, - 'sandbox' => [ - 'methods' => [ - \codename\core\response::class => 'getData' - ] - ] - ]); - - $rendered = $instance->renderSandboxed('test_templates/test_sandboxed_access', [ 'sandboxedVariable' => 'foo' ]); - $this->assertEquals("foo abc\n", $rendered); - } - - /** - * Tests accessing an allowed method access - * in a sandboxed string template succeeds - */ - public function testRenderSandboxedStringWillSucceed(): void { - app::getRequest(); - app::getResponse()->setData('key', 'xyz'); - - $instance = new \codename\core\ui\templateengine\twig([ - 'sandbox_enabled' => true, - 'sandbox' => [ - 'methods' => [ - \codename\core\response::class => 'getData' - ] - ] - ]); - - $rendered = $instance->renderStringSandboxed('{{ sandboxedVariable }} {{ response.getData("key") }}', [ 'sandboxedVariable' => 'foo' ]); - $this->assertEquals("foo xyz", $rendered); - } - - /** - * Tests a simple file inclusion, semi-absolute (relative to project FE root) - * but inherited across apps - */ - public function testIncludedFileTemplate(): void { - $rendered = app::getTemplateEngine()->render('test_templates/example_includes', [ 'example1' => 'abc' ]); - $this->assertEquals("Example1 abc\n", $rendered); - } - - /** - * Special core-ui feature: relative paths may be used in a twig-template - * (relative to current file/cwd) - */ - public function testIncludedRelativeFileTemplate(): void { - $rendered = app::getTemplateEngine()->render('test_templates/example_includes_relative', [ 'example1' => 'abc' ]); - $this->assertEquals("Example1 abc\n", $rendered); - } - - /** - * Tests dynamic (non-sandboxed) template inclusion - * using full context passthrough - */ - public function testIncludeTemplateFromString(): void { - $rendered = app::getTemplateEngine()->render('test_templates/test_include_template_from_string', [ - 'template' => 'TemplateRendered: {{ data.someVariable }}', - 'someVariable' => 'def123', - ]); - $this->assertEquals("TemplateRendered: def123\n", $rendered); - } - - /** - * Tests some core-ui provided Twig-"tests" - * (usage with varname is <...> ) - */ - public function testIntegratedTwigTests(): void { - $instance = app::getTemplateEngine('sandbox_available'); - // - // Tests 'array' test - // - $rendered = $instance->renderStringSandboxed('{{ value is array ? 1 : 0 }}', [ 'value' => [ 1, 2, 3] ]); - $this->assertEquals("1", $rendered); - $rendered = $instance->renderStringSandboxed('{{ value is array ? 1 : 0 }}', [ 'value' => 'abc' ]); - $this->assertEquals("0", $rendered); - - // - // Tests 'string' test - // - $rendered = $instance->renderStringSandboxed('{{ value is string ? 1 : 0 }}', [ 'value' => 123 ]); - $this->assertEquals("0", $rendered); - $rendered = $instance->renderStringSandboxed('{{ value is string ? 1 : 0 }}', [ 'value' => [ 1, 2, 3 ] ]); - $this->assertEquals("0", $rendered); - $rendered = $instance->renderStringSandboxed('{{ value is string ? 1 : 0 }}', [ 'value' => '123' ]); - $this->assertEquals("1", $rendered); - } - - /** - * [testRenderView description] - */ - public function testRenderView(): void { - $this->markTestIncomplete('renderView makes only sense when used in conjunction with app lifecycle'); - } - - /** - * [testRenderTemplate description] - */ - public function testRenderTemplate(): void { - $this->markTestIncomplete('renderView makes only sense when used in conjunction with app lifecycle'); - } + /** + * [testRenderTemplate description] + */ + public function testRenderTemplate(): void + { + static::markTestIncomplete('renderView makes only sense when used in conjunction with app lifecycle'); + } + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws Throwable + * @throws ErrorException + * @throws compileErrorException + * @throws coreErrorException + * @throws coreWarningException + * @throws parseException + * @throws recoverableErrorException + * @throws strictException + * @throws userErrorException + * @throws userWarningException + * @throws warningException + * @throws \codename\core\exception + */ + protected function setUp(): void + { + overrideableApp::resetRequest(); + overrideableApp::resetResponse(); + // reset, to also reset twig instances + // and their stored request/response instances, + // which might be recreated/reset during tests anyway + overrideableApp::reset(); + $app = static::createApp(); + + // Additional overrides to get a more complete app lifecycle + // and allow static global app::getModel() to work correctly + $app::__setApp('twigtest'); + $app::__setVendor('codename'); + $app::__setNamespace('\\codename\\core\\ui\\tests\\templateengine\\twig'); + $app::__setHomedir(__DIR__); + + $app::getAppstack(); + + // NOTE: if we reset the app in setUp(), we have to execute this initialization routine again, no matter what. + // avoid re-init + // if(static::$initialized) { + // return; + // } + + static::$initialized = true; + + static::setEnvironmentConfig([ + 'test' => [ + 'filesystem' => [ + 'local' => [ + 'driver' => 'local', + ], + ], + 'translate' => [ + 'default' => [ + 'driver' => 'dummy', + ], + ], + 'templateengine' => [ + 'default' => [ + 'driver' => 'twig', + ], + 'sandbox_available' => [ + 'driver' => 'twig', + 'sandbox_enabled' => true, + 'sandbox_mode' => null, // not globally enabled + ], + 'sandbox_global' => [ + 'driver' => 'twig', + 'sandbox_enabled' => true, + 'sandbox_mode' => 'global', // sandbox for everything + ], + 'otherext' => [ + 'driver' => 'twig', + 'template_file_extension' => '.otherext.twig', + ], + ], + 'log' => [ + 'default' => [ + 'driver' => 'system', + 'data' => [ + 'name' => 'dummy', + ], + ], + ], + ], + ]); + } } diff --git a/translation/de_DE/button.json b/translation/de_DE/button.json index 726c049..4a0d211 100644 --- a/translation/de_DE/button.json +++ b/translation/de_DE/button.json @@ -1,3 +1,3 @@ { - "BTN_SAVE" : "Speichern" + "BTN_SAVE": "Speichern" } diff --git a/translation/de_DE/crud.json b/translation/de_DE/crud.json index 3329af1..4407fd8 100644 --- a/translation/de_DE/crud.json +++ b/translation/de_DE/crud.json @@ -1,8 +1,8 @@ { - "CRUD_CRUD" : "CRUD", - "CRUD_CRUD_CREATE" : "Erstellen", - "CRUD_CRUD_EDIT" : "Bearbeiten", - "CRUD_CRUD_SHOW" : "Anzeigen", - "CRUD_CRUD_DELETE" : "Löschen", - "CRUD_CRUD_LIST" : "Liste" + "CRUD_CRUD": "CRUD", + "CRUD_CRUD_CREATE": "Erstellen", + "CRUD_CRUD_EDIT": "Bearbeiten", + "CRUD_CRUD_SHOW": "Anzeigen", + "CRUD_CRUD_DELETE": "Löschen", + "CRUD_CRUD_LIST": "Liste" }