diff --git a/src/Constants/PicoHttpStatus.php b/src/Constants/PicoHttpStatus.php index 3d0a3acd..b38c2092 100644 --- a/src/Constants/PicoHttpStatus.php +++ b/src/Constants/PicoHttpStatus.php @@ -1,4 +1,5 @@ of the table. - * @param array $props Array of ReflectionProperty objects representing class properties. + * @param ReflectionProperty[] $props Array of ReflectionProperty objects. * @param string $className Name of the class for reflection. * @return self Returns the current instance for method chaining. */ @@ -353,33 +350,51 @@ private function appendByProp($doc, $tbody, $props, $className) { foreach ($props as $prop) { $key = $prop->name; - $label = $key; $value = $this->get($key); - if (is_scalar($value)) { - $tr = $tbody->appendChild($doc->createElement(self::TAG_TR)); - - $reflexProp = new PicoAnnotationParser($className, $key, PicoAnnotationParser::PROPERTY); - if ($reflexProp != null) { - $parameters = $reflexProp->getParametersAsObject(); - if ($parameters->issetLabel()) { - $label = $this->label($reflexProp, $parameters, $key, $label); + // Inclusion of null values often necessary for table structure consistency + if (is_scalar($value) || $value === null) { + $label = $key; + + // Parse annotations for custom label + try { + $reflexProp = new PicoAnnotationParser($className, $key, PicoAnnotationParser::PROPERTY); + if ($reflexProp != null) { + $parameters = $reflexProp->getParametersAsObject(); + // Assuming issetLabel() or similar check exists in your annotation parser + if (method_exists($parameters, 'issetLabel') && $parameters->issetLabel()) { + $label = $this->label($reflexProp, $parameters, $key, $label); + } } + } catch (Exception $e) { + // Fallback to property name if annotation parsing fails + $label = $key; } - $td1 = $tr->appendChild($doc->createElement(self::TAG_TD)); + // Create Row + $tr = $doc->createElement(self::TAG_TR); + $tbody->appendChild($tr); + + // Column 1: Label + $td1 = $doc->createElement(self::TAG_TD); $td1->setAttribute(self::KEY_CLASS, self::TD_LABEL); - $td1->textContent = $label; + $td1->appendChild($doc->createTextNode($label)); + $tr->appendChild($td1); - $td2 = $tr->appendChild($doc->createElement(self::TAG_TD)); + // Column 2: Value + $td2 = $doc->createElement(self::TAG_TD); $td2->setAttribute(self::KEY_CLASS, self::TD_VALUE); - $td2->textContent = isset($value) ? $value : ""; + + // Safe formatting for boolean/null/strings + $displayValue = $this->formatScalarValue($value); + $td2->appendChild($doc->createTextNode($displayValue)); + $tr->appendChild($td2); } } return $this; } - /** + /** * Appends table rows based on provided values. * * This method takes an array of values and creates rows in the table, @@ -387,28 +402,55 @@ private function appendByProp($doc, $tbody, $props, $className) * * @param DOMDocument $doc The DOM document used to create elements. * @param DOMNode $tbody The DOM node representing the
of the table. - * @param stdClass $values Data to append as rows. + * @param array|stdClass $values Data to append as rows. * @return self Returns the current instance for method chaining. */ private function appendByValues($doc, $tbody, $values) { + if (empty($values)) { + return $this; + } + foreach ($values as $propertyName => $value) { - if (is_scalar($value)) { - $tr = $tbody->appendChild($doc->createElement(self::TAG_TR)); - $label = $this->getLabel($propertyName); + // Check if value can be displayed as string + if (is_scalar($value) || $value === null) { + $tr = $doc->createElement(self::TAG_TR); + $tbody->appendChild($tr); - $td1 = $tr->appendChild($doc->createElement(self::TAG_TD)); + $label = $this->getLabel($propertyName); + + // Column 1: Label + $td1 = $doc->createElement(self::TAG_TD); $td1->setAttribute(self::KEY_CLASS, self::TD_LABEL); - $td1->textContent = $label; + // Use createTextNode for safer character handling + $td1->appendChild($doc->createTextNode($label)); + $tr->appendChild($td1); - $td2 = $tr->appendChild($doc->createElement(self::TAG_TD)); + // Column 2: Value + $td2 = $doc->createElement(self::TAG_TD); $td2->setAttribute(self::KEY_CLASS, self::TD_VALUE); - $td2->textContent = isset($value) ? $value : ""; + + // Format boolean or null if necessary + $displayValue = $this->formatScalarValue($value); + $td2->appendChild($doc->createTextNode($displayValue)); + $tr->appendChild($td2); } } return $this; } + /** + * Helper to ensure value is safe string for DOM + * @param mixed $value + * @return string + */ + private function formatScalarValue($value) + { + if ($value === true) return 'true'; + if ($value === false) return 'false'; + return (string)(isset($value) ? $value : ""); + } + /** * Gets the label for a specified property. * diff --git a/src/Database/PicoDatabasePersistence.php b/src/Database/PicoDatabasePersistence.php index 30c5333f..6b836477 100644 --- a/src/Database/PicoDatabasePersistence.php +++ b/src/Database/PicoDatabasePersistence.php @@ -85,6 +85,9 @@ class PicoDatabasePersistence // NOSONAR const COMMA_RETURN = ", \r\n"; const INLINE_TRIM = " \r\n\t "; const ALWAYS_TRUE = "(1=1)"; + + const IS_NULL = " is null"; + const CLAUSE_AND = " and "; /** * Database connection @@ -929,7 +932,7 @@ private function getWhere($info, $queryBuilder) $value = $queryBuilder->escapeValue($value); if(strcasecmp($value, self::KEY_NULL) == 0) { - $wheres[] = $columnName . " is null"; + $wheres[] = $columnName . self::IS_NULL; } else { @@ -940,7 +943,7 @@ private function getWhere($info, $queryBuilder) { throw new NoPrimaryKeyDefinedException("No primary key defined"); } - return implode(" and ", $wheres); + return implode(self::CLAUSE_AND, $wheres); } /** @@ -977,7 +980,7 @@ private function getWhereWithColumns($info, $queryBuilder) $escapedValue = $queryBuilder->escapeValue($value); if(strcasecmp($escapedValue, self::KEY_NULL) == 0) { - $wheres[] = $columnName . " is null"; + $wheres[] = $columnName . self::IS_NULL; $columns[$columnName] = null; } else @@ -992,7 +995,7 @@ private function getWhereWithColumns($info, $queryBuilder) } $result->columns = $columns; - $result->whereClause = implode(" and ", $wheres); + $result->whereClause = implode(self::CLAUSE_AND, $wheres); return $result; } @@ -2150,14 +2153,14 @@ private function createWhereByPrimaryKeys($queryBuilder, $primaryKeys, $property $columnValue = $propertyValues[$index]; if($columnValue === null) { - $wheres[] = $columnName . " is null"; + $wheres[] = $columnName . self::IS_NULL; } else { $wheres[] = $columnName . " = " . $queryBuilder->escapeValue($propertyValues[$index]); } } - $where = implode(" and ", $wheres); + $where = implode(self::CLAUSE_AND, $wheres); if(!$this->isValidFilter($where)) { throw new InvalidFilterException(self::MESSAGE_INVALID_FILTER); diff --git a/src/Database/PicoPageData.php b/src/Database/PicoPageData.php index 818365f1..8f3fe489 100644 --- a/src/Database/PicoPageData.php +++ b/src/Database/PicoPageData.php @@ -303,7 +303,7 @@ public function getResultAsArray() * @param mixed $data The data to convert. Can be a MagicObject, an array, an object, or a scalar value. * @return mixed The converted data as a plain PHP array, stdClass object, or scalar value. */ - protected function magicObjectToArray($data) + protected function magicObjectToArray($data) // NOSONAR { // Null or scalar if (is_null($data) || is_scalar($data)) { @@ -363,8 +363,6 @@ protected function magicObjectToArray($data) return $data; } - - /** * Get the current page number in the pagination context. * diff --git a/src/Database/PicoSpecificationFilter.php b/src/Database/PicoSpecificationFilter.php index 36890fbf..627e3be8 100644 --- a/src/Database/PicoSpecificationFilter.php +++ b/src/Database/PicoSpecificationFilter.php @@ -14,14 +14,14 @@ */ class PicoSpecificationFilter { - const DATA_TYPE_NUMBER = "number"; - const DATA_TYPE_STRING = "string"; - const DATA_TYPE_BOOLEAN = "boolean"; - const DATA_TYPE_ARRAY_NUMBER = "number[]"; - const DATA_TYPE_ARRAY_STRING = "string[]"; + const DATA_TYPE_NUMBER = "number"; + const DATA_TYPE_STRING = "string"; + const DATA_TYPE_BOOLEAN = "boolean"; + const DATA_TYPE_ARRAY_NUMBER = "number[]"; + const DATA_TYPE_ARRAY_STRING = "string[]"; const DATA_TYPE_ARRAY_BOOLEAN = "boolean[]"; - const DATA_TYPE_FULLTEXT = "fulltext"; - const DATA_TYPE_TEXT_EQUALS = "textequals"; + const DATA_TYPE_FULLTEXT = "fulltext"; + const DATA_TYPE_TEXT_EQUALS = "textequals"; /** * The name of the column this filter applies to. diff --git a/src/Database/PicoTableInfoExtended.php b/src/Database/PicoTableInfoExtended.php index 7946c03e..fd85ee47 100644 --- a/src/Database/PicoTableInfoExtended.php +++ b/src/Database/PicoTableInfoExtended.php @@ -15,9 +15,9 @@ */ class PicoTableInfoExtended extends PicoTableInfo { - const NAME = "name"; // Key for the column name + const NAME = "name"; // Key for the column name const PREV_NAME = "prevColumnName"; // Key for the previous column name - const ELEMENT = "element"; // Key for the element + const ELEMENT = "element"; // Key for the element /** * Gets an instance of PicoTableInfoExtended. diff --git a/src/Generator/PicoDatabaseDump.php b/src/Generator/PicoDatabaseDump.php index 0a660c8b..99870726 100644 --- a/src/Generator/PicoDatabaseDump.php +++ b/src/Generator/PicoDatabaseDump.php @@ -48,24 +48,18 @@ class PicoDatabaseDump // NOSONAR protected $columns = array(); /** - * Generates a SQL CREATE TABLE statement based on the provided entity schema. - * * This method detects the database type and utilizes the appropriate utility - * class to format columns, primary keys, and auto-increment constraints. - * It supports MySQL, MariaDB, PostgreSQL, SQLite, and SQL Server. + * Instantiates the appropriate database dump utility based on the database type. * - * @param array $entity The entity schema containing 'name' and 'columns' (an array of column definitions). - * @param string $databaseType The type of database (e.g., PicoDatabaseType::DATABASE_TYPE_MARIADB). - * @param bool $createIfNotExists Whether to add the "IF NOT EXISTS" clause to the CREATE statement. - * @param bool $dropIfExists Whether to prepend a commented-out "DROP TABLE IF EXISTS" statement. - * @param string $engine The storage engine to use (default is 'InnoDB', primarily for MySQL/MariaDB). - * @param string $charset The character set for the table (default is 'utf8mb4'). - * * @return string The generated SQL DDL statement or an empty string if the database type is unsupported. + * This factory method maps specific database engines to their respective + * utility classes (MySQL, PostgreSQL, SQLite, or SQL Server) to handle + * database-specific dumping operations. + * + * @param string $databaseType The type of database (e.g., MySQL, PostgreSQL, SQLite). + * @return PicoDatabaseUtilBase|string Returns an instance of the database utility tool + * or an empty string if the database type is not supported. */ - public function dumpStructureFromSchema($entity, $databaseType, $createIfNotExists = false, $dropIfExists = false, $engine = 'InnoDB', $charset = 'utf8mb4') + private function getDatabaseDumpTool($databaseType) { - $tableName = $entity['name']; - - // 1. Initialize Tool based on Database Type switch ($databaseType) { case PicoDatabaseType::DATABASE_TYPE_MARIADB: case PicoDatabaseType::DATABASE_TYPE_MYSQL: @@ -84,6 +78,29 @@ public function dumpStructureFromSchema($entity, $databaseType, $createIfNotExis default: return ""; } + return $tool; + } + + /** + * Generates a SQL CREATE TABLE statement based on the provided entity schema. + * * This method detects the database type and utilizes the appropriate utility + * class to format columns, primary keys, and auto-increment constraints. + * It supports MySQL, MariaDB, PostgreSQL, SQLite, and SQL Server. + * + * @param array $entity The entity schema containing 'name' and 'columns' (an array of column definitions). + * @param string $databaseType The type of database (e.g., PicoDatabaseType::DATABASE_TYPE_MARIADB). + * @param bool $createIfNotExists Whether to add the "IF NOT EXISTS" clause to the CREATE statement. + * @param bool $dropIfExists Whether to prepend a commented-out "DROP TABLE IF EXISTS" statement. + * @param string $engine The storage engine to use (default is 'InnoDB', primarily for MySQL/MariaDB). + * @param string $charset The character set for the table (default is 'utf8mb4'). + * @return string The generated SQL DDL statement or an empty string if the database type is unsupported. + */ + public function dumpStructureFromSchema($entity, $databaseType, $createIfNotExists = false, $dropIfExists = false, $engine = 'InnoDB', $charset = 'utf8mb4') + { + $tableName = $entity['name']; + + // 1. Initialize Tool based on Database Type + $tool = $this->getDatabaseDumpTool($databaseType); $columns = array(); $primaryKeys = array(); @@ -154,15 +171,11 @@ public function dumpDataFromSchema($entity, $databaseType, $batchSize = 100) { // Check if the target database is PostgreSQL $isPgSql = $databaseType == PicoDatabaseType::DATABASE_TYPE_PGSQL || $databaseType == PicoDatabaseType::DATABASE_TYPE_POSTGRESQL; - $columnInfo = array(); + $tableName = $entity['name']; // 1. Prepare Column Information for type-casting - if (isset($entity['columns']) && is_array($entity['columns'])) { - foreach ($entity['columns'] as $column) { - $columnInfo[$column['name']] = $this->getColumnInfo($column); - } - } + $columnInfo = $this->prepareColumnInfo($entity); $validColumnNames = array_keys($columnInfo); @@ -203,12 +216,34 @@ public function dumpDataFromSchema($entity, $databaseType, $batchSize = 100) . implode(",\r\n", $rows) . ";\r\n\r\n"; } - } return $allSql; } + /** + * Prepares and normalizes column metadata from the entity schema. + * + * This method iterates through the column definitions of an entity and + * transforms them into a structured associative array of column information, + * indexed by the column names. + * + * @param array $entity The entity schema containing the 'columns' definition. + * @return array