From 91f8d3533f2cf19335d98a082201e70173c4a616 Mon Sep 17 00:00:00 2001 From: Belisoful Date: Thu, 22 Oct 2026 01:43:48 -0700 Subject: [PATCH 1/2] Prado 4.4 extension manifest, error messages, class map; port framework SqlMap fixes --- CLAUDE.md | 1 + composer.json | 7 +- config/classes.json | 57 +++++++++++++++ .../SqlMap/Configuration/TParameterMap.php | 1 - src/Data/SqlMap/Configuration/TResultMap.php | 6 +- .../SqlMap/Configuration/TResultProperty.php | 12 ++-- .../Configuration/TSqlMapCacheModel.php | 1 - .../SqlMap/Configuration/TSqlMapStatement.php | 4 -- src/Data/SqlMap/DataMapper/TLazyLoadList.php | 2 +- .../SqlMap/DataMapper/TSqlMapException.php | 40 ++++++----- src/Data/SqlMap/DataMapper/messages.txt | 7 +- .../SqlMap/Statements/TMappedStatement.php | 7 +- .../Statements/TPreparedStatementFactory.php | 1 - .../TSqlMapObjectCollectionTree.php | 13 ++-- src/Data/SqlMap/TSqlMapManager.php | 12 ++-- .../DataMapper/TSqlMapExceptionTest.php | 72 +++++++++++++++++++ 16 files changed, 188 insertions(+), 55 deletions(-) create mode 100644 config/classes.json create mode 100644 tests/unit/Data/SqlMap/DataMapper/TSqlMapExceptionTest.php diff --git a/CLAUDE.md b/CLAUDE.md index ee1e915..f7778f3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,6 +161,7 @@ tests/ ## Important Rules - **No `classes.php`** — this is a standalone extension; the PSR-4 autoloader handles class discovery. Do not create a `classes.php`. +- **`composer.json` `extra.prado`** (Prado 4.4) declares `bootstrap`, `error-messages` (`src/Data/SqlMap/DataMapper/messages.txt`), and `class-map` (`config/classes.json`, short name → FQCN for XML `class="TSqlMapConfig"` style references). Keep the legacy top-level `extra.bootstrap` for Prado 4.3. Add new classes to `config/classes.json`. - **`if` statements** always use a block (`{}`), never a single-line body. - **Backward compatibility** — all changes must be backward compatible for point releases. - **`@since` tag** — use the next release version when adding new methods or classes; omit the method tag when it matches the class tag. Check prado's `CLAUDE.md` for the current next-release version number. diff --git a/composer.json b/composer.json index 3c850b5..5f2de5e 100644 --- a/composer.json +++ b/composer.json @@ -76,6 +76,11 @@ ] }, "extra": { - "bootstrap": "Prado\\Data\\SqlMap\\TSqlMapConfig" + "bootstrap": "Prado\\Data\\SqlMap\\TSqlMapConfig", + "prado": { + "bootstrap": "Prado\\Data\\SqlMap\\TSqlMapConfig", + "error-messages": "src/Data/SqlMap/DataMapper/messages.txt", + "class-map": "config/classes.json" + } } } diff --git a/config/classes.json b/config/classes.json new file mode 100644 index 0000000..e98a457 --- /dev/null +++ b/config/classes.json @@ -0,0 +1,57 @@ +{ + "TDiscriminator": "Prado\\Data\\SqlMap\\Configuration\\TDiscriminator", + "TInlineParameterMapParser": "Prado\\Data\\SqlMap\\Configuration\\TInlineParameterMapParser", + "TParameterMap": "Prado\\Data\\SqlMap\\Configuration\\TParameterMap", + "TParameterProperty": "Prado\\Data\\SqlMap\\Configuration\\TParameterProperty", + "TResultMap": "Prado\\Data\\SqlMap\\Configuration\\TResultMap", + "TResultProperty": "Prado\\Data\\SqlMap\\Configuration\\TResultProperty", + "TSimpleDynamicParser": "Prado\\Data\\SqlMap\\Configuration\\TSimpleDynamicParser", + "TSqlMapCacheKey": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapCacheKey", + "TSqlMapCacheModel": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapCacheModel", + "TSqlMapCacheTypes": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapCacheTypes", + "TSqlMapDelete": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapDelete", + "TSqlMapInsert": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapInsert", + "TSqlMapSelect": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapSelect", + "TSqlMapSelectKey": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapSelectKey", + "TSqlMapStatement": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapStatement", + "TSqlMapUpdate": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapUpdate", + "TSqlMapXmlConfigBuilder": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapXmlConfigBuilder", + "TSqlMapXmlConfiguration": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapXmlConfiguration", + "TSqlMapXmlMappingConfiguration": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapXmlMappingConfiguration", + "TSubMap": "Prado\\Data\\SqlMap\\Configuration\\TSubMap", + "TInvalidPropertyException": "Prado\\Data\\SqlMap\\DataMapper\\TInvalidPropertyException", + "TLazyLoadList": "Prado\\Data\\SqlMap\\DataMapper\\TLazyLoadList", + "TObjectProxy": "Prado\\Data\\SqlMap\\DataMapper\\TObjectProxy", + "TPropertyAccess": "Prado\\Data\\SqlMap\\DataMapper\\TPropertyAccess", + "TSqlMapApplicationCache": "Prado\\Data\\SqlMap\\DataMapper\\TSqlMapApplicationCache", + "TSqlMapCache": "Prado\\Data\\SqlMap\\DataMapper\\TSqlMapCache", + "TSqlMapConfigurationException": "Prado\\Data\\SqlMap\\DataMapper\\TSqlMapConfigurationException", + "TSqlMapDuplicateException": "Prado\\Data\\SqlMap\\DataMapper\\TSqlMapDuplicateException", + "TSqlMapException": "Prado\\Data\\SqlMap\\DataMapper\\TSqlMapException", + "TSqlMapExecutionException": "Prado\\Data\\SqlMap\\DataMapper\\TSqlMapExecutionException", + "TSqlMapFifoCache": "Prado\\Data\\SqlMap\\DataMapper\\TSqlMapFifoCache", + "TSqlMapLruCache": "Prado\\Data\\SqlMap\\DataMapper\\TSqlMapLruCache", + "TSqlMapPagedList": "Prado\\Data\\SqlMap\\DataMapper\\TSqlMapPagedList", + "TSqlMapTypeHandler": "Prado\\Data\\SqlMap\\DataMapper\\TSqlMapTypeHandler", + "TSqlMapTypeHandlerRegistry": "Prado\\Data\\SqlMap\\DataMapper\\TSqlMapTypeHandlerRegistry", + "TSqlMapUndefinedException": "Prado\\Data\\SqlMap\\DataMapper\\TSqlMapUndefinedException", + "IMappedStatement": "Prado\\Data\\SqlMap\\Statements\\IMappedStatement", + "TCachingStatement": "Prado\\Data\\SqlMap\\Statements\\TCachingStatement", + "TDeleteMappedStatement": "Prado\\Data\\SqlMap\\Statements\\TDeleteMappedStatement", + "TInsertMappedStatement": "Prado\\Data\\SqlMap\\Statements\\TInsertMappedStatement", + "TMappedStatement": "Prado\\Data\\SqlMap\\Statements\\TMappedStatement", + "TPostSelectBinding": "Prado\\Data\\SqlMap\\Statements\\TPostSelectBinding", + "TPreparedCommand": "Prado\\Data\\SqlMap\\Statements\\TPreparedCommand", + "TPreparedStatement": "Prado\\Data\\SqlMap\\Statements\\TPreparedStatement", + "TPreparedStatementFactory": "Prado\\Data\\SqlMap\\Statements\\TPreparedStatementFactory", + "TResultSetListItemParameter": "Prado\\Data\\SqlMap\\Statements\\TResultSetListItemParameter", + "TResultSetMapItemParameter": "Prado\\Data\\SqlMap\\Statements\\TResultSetMapItemParameter", + "TSelectMappedStatement": "Prado\\Data\\SqlMap\\Statements\\TSelectMappedStatement", + "TSimpleDynamicSql": "Prado\\Data\\SqlMap\\Statements\\TSimpleDynamicSql", + "TSqlMapObjectCollectionTree": "Prado\\Data\\SqlMap\\Statements\\TSqlMapObjectCollectionTree", + "TStaticSql": "Prado\\Data\\SqlMap\\Statements\\TStaticSql", + "TUpdateMappedStatement": "Prado\\Data\\SqlMap\\Statements\\TUpdateMappedStatement", + "TSqlMapConfig": "Prado\\Data\\SqlMap\\TSqlMapConfig", + "TSqlMapGateway": "Prado\\Data\\SqlMap\\TSqlMapGateway", + "TSqlMapManager": "Prado\\Data\\SqlMap\\TSqlMapManager" +} diff --git a/src/Data/SqlMap/Configuration/TParameterMap.php b/src/Data/SqlMap/Configuration/TParameterMap.php index e3ec801..63264b1 100644 --- a/src/Data/SqlMap/Configuration/TParameterMap.php +++ b/src/Data/SqlMap/Configuration/TParameterMap.php @@ -40,7 +40,6 @@ class TParameterMap extends \Prado\TComponent private $_extend; private $_properties; private $_propertyMap; - private $_extendMap; private $_ID; /** diff --git a/src/Data/SqlMap/Configuration/TResultMap.php b/src/Data/SqlMap/Configuration/TResultMap.php index 1e9471e..188e011 100644 --- a/src/Data/SqlMap/Configuration/TResultMap.php +++ b/src/Data/SqlMap/Configuration/TResultMap.php @@ -44,7 +44,6 @@ class TResultMap extends \Prado\TComponent private $_extends; private $_groupBy; private $_discriminator; - private $_typeHandlers; private $_ID; /** @@ -162,17 +161,18 @@ public function addResultProperty(TResultProperty $property) public function createInstanceOfResult($registry) { $handler = $registry->getTypeHandler($this->getClass()); + $handlerClass = $handler !== null ? $handler::class : 'null'; try { if ($handler !== null) { return $handler->createNewInstance(); } else { return $registry->createInstanceOf($this->getClass()); } - } catch (TSqlMapException $e) { + } catch (TSqlMapException) { throw new TSqlMapException( 'sqlmap_unable_to_create_new_instance', $this->getClass(), - $handler ? $handler::class : 'null', + $handlerClass, $this->getID() ); } diff --git a/src/Data/SqlMap/Configuration/TResultProperty.php b/src/Data/SqlMap/Configuration/TResultProperty.php index 18d8447..3cb690f 100644 --- a/src/Data/SqlMap/Configuration/TResultProperty.php +++ b/src/Data/SqlMap/Configuration/TResultProperty.php @@ -50,20 +50,18 @@ class TResultProperty extends \Prado\TComponent private $_isLazyLoad = false; private $_select; - private $_hostResultMapID = 'inplicit internal mapping'; - public const LIST_TYPE = 0; public const ARRAY_TYPE = 1; /** - * Gets the containing result map ID. - * @param TResultMap $resultMap containing result map. + * Initializes a result property. + * + * The optional result map parameter is retained for backward compatibility. + * @param ?TResultMap $resultMap legacy containing result map. + * @phpstan-ignore constructor.unusedParameter (kept for callers that pass the containing result map) */ public function __construct($resultMap = null) { - if ($resultMap instanceof TResultMap) { - $this->_hostResultMapID = $resultMap->getID(); - } parent::__construct(); } diff --git a/src/Data/SqlMap/Configuration/TSqlMapCacheModel.php b/src/Data/SqlMap/Configuration/TSqlMapCacheModel.php index f3325a1..2d6a487 100644 --- a/src/Data/SqlMap/Configuration/TSqlMapCacheModel.php +++ b/src/Data/SqlMap/Configuration/TSqlMapCacheModel.php @@ -40,7 +40,6 @@ class TSqlMapCacheModel extends \Prado\TComponent private $_requests = 0; private $_id; private $_implementation = TSqlMapCacheTypes::Basic; - private $_properties = []; private $_flushInterval = 0; private static $_cacheTypes = []; diff --git a/src/Data/SqlMap/Configuration/TSqlMapStatement.php b/src/Data/SqlMap/Configuration/TSqlMapStatement.php index c97111c..ef193bb 100644 --- a/src/Data/SqlMap/Configuration/TSqlMapStatement.php +++ b/src/Data/SqlMap/Configuration/TSqlMapStatement.php @@ -41,7 +41,6 @@ class TSqlMapStatement extends \Prado\TComponent private $_cacheModelName; private $_SQL; private $_listClass; - private $_typeHandler; private $_extendStatement; private $_cache; private $_ID; @@ -332,9 +331,6 @@ public function __sleep() if (!$this->_listClass) { $exprops[] = "\0$cn\0_listClass"; } - if (!$this->_typeHandler) { - $exprops[] = "\0$cn\0_typeHandler"; - } if (!$this->_extendStatement) { $exprops[] = "\0$cn\0_extendStatement"; } diff --git a/src/Data/SqlMap/DataMapper/TLazyLoadList.php b/src/Data/SqlMap/DataMapper/TLazyLoadList.php index 741b67c..bad8c8c 100644 --- a/src/Data/SqlMap/DataMapper/TLazyLoadList.php +++ b/src/Data/SqlMap/DataMapper/TLazyLoadList.php @@ -80,7 +80,7 @@ public function intercept($method, $arguments) */ protected function fetchListData() { - if ($this->_loaded == false) { + if (!$this->_loaded) { $this->_innerList = $this->_statement->executeQueryForList($this->_connection, $this->_param); $this->_loaded = true; //replace the target property with real list diff --git a/src/Data/SqlMap/DataMapper/TSqlMapException.php b/src/Data/SqlMap/DataMapper/TSqlMapException.php index 986a467..11f23a6 100644 --- a/src/Data/SqlMap/DataMapper/TSqlMapException.php +++ b/src/Data/SqlMap/DataMapper/TSqlMapException.php @@ -4,7 +4,6 @@ use Prado\Exceptions\TException; use Prado\Prado; -use Prado\TPropertyValue; use SimpleXMLElement; /** @@ -16,27 +15,32 @@ class TSqlMapException extends TException { /** - * Constructor, similar to the parent constructor. For parameters that - * are of SimpleXmlElement, the tag name and its attribute names and values - * are expanded into a string. - * @param mixed $errorMessage + * Constructor. The arguments follow {@see \Prado\Exceptions\TException::__construct()}. + * - `new TSqlMapException('message_code', $param0, ...)` → error code 0. + * - `new TSqlMapException($errorCode, 'message_code', $param0, ...)` → integer error code. + * - A trailing {@see \Throwable} argument is the previous exception in the chain. + * - A {@see \SimpleXMLElement} parameter expands to its tag name with attribute names and values. + * @param int|string $errorCode the integer error code, or the message code in the old style. + * @param mixed $errorMessage the message code when $errorCode is an integer, otherwise + * the first message parameter. + * @param mixed ...$args the remaining message parameters, optionally followed by the + * previous exception. */ - public function __construct($errorMessage) + public function __construct($errorCode, $errorMessage = null, ...$args) { - $this->setErrorCode($errorMessage); - $errorMessage = $this->translateErrorMessage($errorMessage); - $args = func_get_args(); - array_shift($args); - $n = count($args); - $tokens = []; - for ($i = 0; $i < $n; ++$i) { - if ($args[$i] instanceof SimpleXMLElement) { - $tokens['{' . $i . '}'] = $this->implodeNode($args[$i]); - } else { - $tokens['{' . $i . '}'] = TPropertyValue::ensureString($args[$i]); + if (!is_int($errorCode)) { + if (func_num_args() > 1) { + array_unshift($args, $errorMessage); } + $errorMessage = $errorCode; + $errorCode = 0; } - parent::__construct(strtr($errorMessage, $tokens)); + foreach ($args as $i => $arg) { + if ($arg instanceof SimpleXMLElement) { + $args[$i] = $this->implodeNode($arg); + } + } + parent::__construct($errorCode, $errorMessage, ...$args); } /** diff --git a/src/Data/SqlMap/DataMapper/messages.txt b/src/Data/SqlMap/DataMapper/messages.txt index 0923d60..ad7cb9c 100644 --- a/src/Data/SqlMap/DataMapper/messages.txt +++ b/src/Data/SqlMap/DataMapper/messages.txt @@ -63,4 +63,9 @@ sqlmap_query_execution_error = Error in executing SQLMap statement '{0}' : '{1 sqlmap_invalid_delegate = Invalid callback row delegate '{1}' in mapped statement '{0}'. sqlmap_invalid_prado_cache = Unable to find Prado cache module for SQLMap cache '{0}'. -sqlmap_non_groupby_array_list_type = Expecting GroupBy property in result map '{0}' since {1}::{2} is an array or TList type. \ No newline at end of file +sqlmap_non_groupby_array_list_type = Expecting GroupBy property in result map '{0}' since {1}::{2} is an array or TList type. +sqlmap_can_not_extend_select_key = SelectKey statements do not support inheritance via the 'extends' attribute. +sqlmap_configfile_invalid = SQLMap configuration file or namespace '{0}' is invalid or does not exist. +sqlmap_must_enable_custom_paging = TSqlMapPagedList always uses custom paging; CustomPaging cannot be changed. +sqlmap_property_must_be_list = A grouped result property must be an array or TList to hold child objects. +sqlmap_use_set_to_store_cache = Use set() to store items in the SQLMap cache; add() is not supported. diff --git a/src/Data/SqlMap/Statements/TMappedStatement.php b/src/Data/SqlMap/Statements/TMappedStatement.php index 0a4e8cf..671c738 100644 --- a/src/Data/SqlMap/Statements/TMappedStatement.php +++ b/src/Data/SqlMap/Statements/TMappedStatement.php @@ -805,6 +805,7 @@ protected function getScalarResult($result, $type) * @param TResultProperty $property the result property to fill. * @param array $row a result set row retrieved from the database * @param object $resultObject the result object + * @phpstan-impure */ protected function setObjectProperty($resultMap, $property, $row, &$resultObject) { @@ -835,7 +836,7 @@ protected function setObjectProperty($resultMap, $property, $row, &$resultObject } } else { $obj = $nested->createInstanceOfResult($this->getManager()->getTypeHandlers()); - if ($this->fillPropertyWithResultMap($nested, $row, $obj) == false) { + if (!$this->fillPropertyWithResultMap($nested, $row, $obj)) { $obj = null; } TPropertyAccess::set($resultObject, $key, $obj); @@ -935,7 +936,9 @@ protected function fillPropertyWithResultMap($resultMap, $row, &$resultObject) foreach ($resultMap->getColumns() as $property) { $this->_IsRowDataFound = false; $this->setObjectProperty($resultMap, $property, $row, $resultObject); - $dataFound = $dataFound || $this->_IsRowDataFound; + if ($this->_IsRowDataFound) { + $dataFound = true; + } } $this->_IsRowDataFound = $dataFound; return $dataFound; diff --git a/src/Data/SqlMap/Statements/TPreparedStatementFactory.php b/src/Data/SqlMap/Statements/TPreparedStatementFactory.php index af9b4f0..3a4fc1d 100644 --- a/src/Data/SqlMap/Statements/TPreparedStatementFactory.php +++ b/src/Data/SqlMap/Statements/TPreparedStatementFactory.php @@ -20,7 +20,6 @@ class TPreparedStatementFactory { private $_statement; private $_preparedStatement; - private $_parameterPrefix = 'param'; private $_commandText; public function __construct($statement, $sqlString) diff --git a/src/Data/SqlMap/Statements/TSqlMapObjectCollectionTree.php b/src/Data/SqlMap/Statements/TSqlMapObjectCollectionTree.php index a621081..01694b5 100644 --- a/src/Data/SqlMap/Statements/TSqlMapObjectCollectionTree.php +++ b/src/Data/SqlMap/Statements/TSqlMapObjectCollectionTree.php @@ -55,8 +55,7 @@ public function isEmpty() */ public function add($parent, $node, $object = '') { - if (isset($this->_entries[$parent]) && ($this->_entries[$parent] !== null) - && isset($this->_entries[$node]) && ($this->_entries[$node] !== null)) { + if (isset($this->_entries[$parent]) && isset($this->_entries[$node])) { $this->_entries[$node] = $object; return; } @@ -87,11 +86,8 @@ public function add($parent, $node, $object = '') protected function addNode(&$childs, $parent, $node) { $found = false; - reset($childs); - for ($i = 0, $k = count($childs); $i < $k; $i++) { - $key = key($childs); - next($childs); - if ($key == $parent) { + foreach (array_keys($childs) as $key) { + if ((string) $key === (string) $parent) { $found = true; $childs[$key][$node] = []; } else { @@ -136,8 +132,7 @@ protected function collectChildren($parent, &$nodes) { $noChildren = !$this->hasChildren($nodes); $childs = []; - for (reset($nodes); $key = key($nodes);) { - next($nodes); + foreach (array_keys($nodes) as $key) { if ($noChildren) { $childs[] = $key; unset($nodes[$key]); diff --git a/src/Data/SqlMap/TSqlMapManager.php b/src/Data/SqlMap/TSqlMapManager.php index 91fe8a9..84cd621 100644 --- a/src/Data/SqlMap/TSqlMapManager.php +++ b/src/Data/SqlMap/TSqlMapManager.php @@ -155,7 +155,7 @@ public function getMappedStatements() */ public function getMappedStatement($name) { - if ($this->_mappedStatements->contains($name) == false) { + if (!$this->_mappedStatements->contains($name)) { throw new TSqlMapUndefinedException('sqlmap_contains_no_statement', $name); } return $this->_mappedStatements[$name]; @@ -169,7 +169,7 @@ public function getMappedStatement($name) public function addMappedStatement(IMappedStatement $statement) { $key = $statement->getID(); - if ($this->_mappedStatements->contains($key) == true) { + if ($this->_mappedStatements->contains($key)) { throw new TSqlMapDuplicateException('sqlmap_already_contains_statement', $key); } $this->_mappedStatements->add($key, $statement); @@ -191,7 +191,7 @@ public function getResultMaps() */ public function getResultMap($name) { - if ($this->_resultMaps->contains($name) == false) { + if (!$this->_resultMaps->contains($name)) { throw new TSqlMapUndefinedException('sqlmap_contains_no_result_map', $name); } return $this->_resultMaps[$name]; @@ -204,7 +204,7 @@ public function getResultMap($name) public function addResultMap(TResultMap $result) { $key = $result->getID(); - if ($this->_resultMaps->contains($key) == true) { + if ($this->_resultMaps->contains($key)) { throw new TSqlMapDuplicateException('sqlmap_already_contains_result_map', $key); } $this->_resultMaps->add($key, $result); @@ -225,7 +225,7 @@ public function getParameterMaps() */ public function getParameterMap($name) { - if ($this->_parameterMaps->contains($name) == false) { + if (!$this->_parameterMaps->contains($name)) { throw new TSqlMapUndefinedException('sqlmap_contains_no_parameter_map', $name); } return $this->_parameterMaps[$name]; @@ -238,7 +238,7 @@ public function getParameterMap($name) public function addParameterMap(TParameterMap $parameter) { $key = $parameter->getID(); - if ($this->_parameterMaps->contains($key) == true) { + if ($this->_parameterMaps->contains($key)) { throw new TSqlMapDuplicateException('sqlmap_already_contains_parameter_map', $key); } $this->_parameterMaps->add($key, $parameter); diff --git a/tests/unit/Data/SqlMap/DataMapper/TSqlMapExceptionTest.php b/tests/unit/Data/SqlMap/DataMapper/TSqlMapExceptionTest.php new file mode 100644 index 0000000..b074767 --- /dev/null +++ b/tests/unit/Data/SqlMap/DataMapper/TSqlMapExceptionTest.php @@ -0,0 +1,72 @@ +assertSame("Unable to find SQLMap statement 'findUser'.", $e->getMessage()); + $this->assertSame(0, $e->getCode()); + $this->assertSame('sqlmap_contains_no_statement', $e->getErrorCode()); + $this->assertNull($e->getPrevious()); + } + + public function testOldStyleWithoutParameters() + { + $e = new TSqlMapException('sqlmap_use_set_to_store_cache'); + $this->assertSame('Use set() to store items in the SQLMap cache; add() is not supported.', $e->getMessage()); + } + + public function testOldStyleKeepsExplicitNullParameter() + { + $e = new TSqlMapException('sqlmap_unable_to_create_new_instance', null, 'Handler', 'stmt'); + $this->assertSame("Unable to create a new instance of '' using type hander 'Handler' for SQLMap statement with ID 'stmt'.", $e->getMessage()); + } + + public function testIntegerErrorCode() + { + $e = new TSqlMapException(42, 'sqlmap_contains_no_result_map', 'Account'); + $this->assertSame("Unable to find SQLMap result map 'Account'.", $e->getMessage()); + $this->assertSame(42, $e->getCode()); + $this->assertSame('sqlmap_contains_no_result_map', $e->getErrorCode()); + } + + public function testTrailingThrowableIsPrevious() + { + $previous = new RuntimeException('inner'); + $e = new TSqlMapException('sqlmap_contains_no_statement', 'findUser', $previous); + $this->assertSame($previous, $e->getPrevious()); + $this->assertSame("Unable to find SQLMap statement 'findUser'.", $e->getMessage()); + + $e = new TSqlMapException(7, 'sqlmap_contains_no_statement', 'findUser', $previous); + $this->assertSame($previous, $e->getPrevious()); + $this->assertSame(7, $e->getCode()); + } + + public function testSimpleXmlElementParameterExpands() + { + $node = new SimpleXMLElement(' in file file.xml.", + $e->getMessage() + ); + } + + public function testMessagesNotInFrameworkFileTranslate() + { + $snapshot = PradoUnit::snapshotStatic(TException::class); + try { + TException::addMessageFile(dirname(__DIR__, 5) . '/src/Data/SqlMap/DataMapper/messages.txt'); + $e = new \Prado\Exceptions\TConfigurationException('sqlmap_configfile_invalid', 'Application.missing'); + $this->assertSame("SQLMap configuration file or namespace 'Application.missing' is invalid or does not exist.", $e->getMessage()); + } finally { + PradoUnit::restoreStatic(TException::class, $snapshot); + } + } +} From e74667ae6b198c2696312ca0cd132b203ed693fa Mon Sep 17 00:00:00 2001 From: Belisoful Date: Thu, 22 Oct 2026 01:57:54 -0700 Subject: [PATCH 2/2] Register class map in test bootstrap; port invokable delegate and PHP 8.5 null-key fixes --- .../DataMapper/TSqlMapTypeHandlerRegistry.php | 6 +- .../SqlMap/Statements/TMappedStatement.php | 8 +-- .../TSqlMapObjectCollectionTree.php | 4 +- tests/test_tools/phpunit_bootstrap.php | 7 ++ .../TMappedStatementDelegateTest.php | 69 +++++++++++++++++++ 5 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 tests/unit/Data/SqlMap/Statements/TMappedStatementDelegateTest.php diff --git a/src/Data/SqlMap/DataMapper/TSqlMapTypeHandlerRegistry.php b/src/Data/SqlMap/DataMapper/TSqlMapTypeHandlerRegistry.php index ba89a75..0403490 100644 --- a/src/Data/SqlMap/DataMapper/TSqlMapTypeHandlerRegistry.php +++ b/src/Data/SqlMap/DataMapper/TSqlMapTypeHandlerRegistry.php @@ -38,12 +38,12 @@ public function getDbTypeHandler($dbType = 'NULL') } /** - * @param string $class type handler class name - * @return null|TSqlMapTypeHandler type handler + * @param ?string $class type handler class name + * @return null|TSqlMapTypeHandler type handler, null when $class is null or unregistered */ public function getTypeHandler($class) { - if (isset($this->_typeHandlers[$class])) { + if ($class !== null && isset($this->_typeHandlers[$class])) { return $this->_typeHandlers[$class]; } return null; diff --git a/src/Data/SqlMap/Statements/TMappedStatement.php b/src/Data/SqlMap/Statements/TMappedStatement.php index 671c738..604d59f 100644 --- a/src/Data/SqlMap/Statements/TMappedStatement.php +++ b/src/Data/SqlMap/Statements/TMappedStatement.php @@ -295,9 +295,9 @@ protected function raiseRowDelegate($handler, $param) { if ($handler instanceof \Closure) { $handler($this, $param); - } elseif (is_string($handler)) { + } elseif (is_string($handler) || is_object($handler) && is_callable($handler)) { call_user_func($handler, $this, $param); - } elseif (is_callable($handler, true)) { + } elseif (is_array($handler) && is_callable($handler, true)) { // an array: 0 - object, 1 - method name/path [$object, $method] = $handler; if (is_string($object)) { // static method call @@ -423,7 +423,7 @@ protected function getPostGeneratedSelectKey($connection, $parameter) * @param \Prado\Data\TDbConnection $connection database connection * @param mixed $parameter insert statement parameter * @param TSqlMapSelectKey $selectKey select key statement - * @return string last insert ID. + * @return mixed last insert ID. */ protected function executeSelectKey($connection, $parameter, $selectKey) { @@ -498,7 +498,7 @@ public function onExecuteQuery($sql) * pages through a ROW_NUMBER() subquery, and its outer "SELECT *" returns that helper column. * @param array $row a result set row retrieved from the database * @param null|object $resultObject the result object, will create if necessary. - * @return object the result filled with data, null if not filled. + * @return ?object the result filled with data, null if not filled. */ protected function applyResultMap($row, &$resultObject = null) { diff --git a/src/Data/SqlMap/Statements/TSqlMapObjectCollectionTree.php b/src/Data/SqlMap/Statements/TSqlMapObjectCollectionTree.php index 01694b5..3d7cdcb 100644 --- a/src/Data/SqlMap/Statements/TSqlMapObjectCollectionTree.php +++ b/src/Data/SqlMap/Statements/TSqlMapObjectCollectionTree.php @@ -49,13 +49,13 @@ public function isEmpty() /** * Add a new node to the object tree graph. - * @param string $parent parent node id + * @param ?string $parent parent node id, null for a root node * @param string $node new node id * @param mixed $object node value */ public function add($parent, $node, $object = '') { - if (isset($this->_entries[$parent]) && isset($this->_entries[$node])) { + if ($parent !== null && isset($this->_entries[$parent]) && isset($this->_entries[$node])) { $this->_entries[$node] = $object; return; } diff --git a/tests/test_tools/phpunit_bootstrap.php b/tests/test_tools/phpunit_bootstrap.php index 714c89f..07238c4 100644 --- a/tests/test_tools/phpunit_bootstrap.php +++ b/tests/test_tools/phpunit_bootstrap.php @@ -4,3 +4,10 @@ require_once(__DIR__ . '/../../vendor/pradosoft/prado/framework/Prado.php'); $application = new \Prado\Test\Unit\Harness\TTestApplication(); + +// Prado applies extra.prado class maps and message files only for installed packages. +// The extension under test is the root package, so they are registered here. +if (method_exists(\Prado\Prado::class, 'registerClassMap')) { + \Prado\Prado::registerClassMap(json_decode(file_get_contents(__DIR__ . '/../../config/classes.json'), true)); +} +\Prado\Exceptions\TException::addMessageFile(__DIR__ . '/../../src/Data/SqlMap/DataMapper/messages.txt'); diff --git a/tests/unit/Data/SqlMap/Statements/TMappedStatementDelegateTest.php b/tests/unit/Data/SqlMap/Statements/TMappedStatementDelegateTest.php new file mode 100644 index 0000000..6202be5 --- /dev/null +++ b/tests/unit/Data/SqlMap/Statements/TMappedStatementDelegateTest.php @@ -0,0 +1,69 @@ +calls[] = [$sender, $param]; + } +} + +class TMappedStatementDelegateTest extends PHPUnit\Framework\TestCase +{ + private function raise(TMappedStatement $statement, $handler, $param): void + { + $method = new ReflectionMethod($statement, 'raiseRowDelegate'); + $method->invoke($statement, $handler, $param); + } + + private function newStatement(): TMappedStatement + { + return (new ReflectionClass(TMappedStatement::class))->newInstanceWithoutConstructor(); + } + + public function testInvokableObjectDelegate() + { + $statement = $this->newStatement(); + $handler = new TMappedStatementDelegateInvokable(); + $this->raise($statement, $handler, 'row'); + $this->assertSame([[$statement, 'row']], $handler->calls); + } + + public function testClosureDelegate() + { + $statement = $this->newStatement(); + $calls = []; + $this->raise($statement, function ($sender, $param) use (&$calls) { + $calls[] = [$sender, $param]; + }, 'row'); + $this->assertSame([[$statement, 'row']], $calls); + } + + public function testArrayDelegate() + { + $statement = $this->newStatement(); + $handler = new TMappedStatementDelegateInvokable(); + $this->raise($statement, [$handler, '__invoke'], 'row'); + $this->assertSame([[$statement, 'row']], $handler->calls); + } + + public function testTypeHandlerRegistryNullClass() + { + $this->assertNull((new TSqlMapTypeHandlerRegistry())->getTypeHandler(null)); + } + + public function testCollectionTreeNullParent() + { + $tree = new TSqlMapObjectCollectionTree(); + $tree->add(null, 'root', 'object'); + $tree->add(null, 'root', 'replaced'); + $this->assertSame('replaced', PradoUnit::getProp($tree, '_entries')['root']); + } +}