diff --git a/src/Propel/Generator/Builder/Om/TableMapLoaderScriptBuilder.php b/src/Propel/Generator/Builder/Om/TableMapLoaderScriptBuilder.php index 365224ab4b..026b7ae00b 100644 --- a/src/Propel/Generator/Builder/Om/TableMapLoaderScriptBuilder.php +++ b/src/Propel/Generator/Builder/Om/TableMapLoaderScriptBuilder.php @@ -11,7 +11,10 @@ use Propel\Common\Util\PathTrait; use Propel\Generator\Builder\Util\PropelTemplate; use Propel\Generator\Config\GeneratorConfigInterface; +use Propel\Generator\Exception\BuildException; +use Propel\Generator\Model\Database; use Propel\Generator\Model\Table; +use Propel\Runtime\Map\DatabaseMap; use SplFileInfo; /** @@ -46,65 +49,86 @@ public function __construct(GeneratorConfigInterface $generatorConfig) */ public function build(array $schemas): string { - $vars = $this->buildVars($schemas); + $templatePath = $this->getTemplatePath(__DIR__); + + $filePath = $templatePath . 'tableMapLoaderScript.php'; + $template = new PropelTemplate(); + $template->setTemplateFile($filePath); - return $this->renderTemplate($vars); + $vars = [ + 'databaseNameToTableMapDumps' => $this->buildDatabaseNameToTableMapDumps($schemas), + ]; + + return $template->render($vars); } /** * @param array<\Propel\Generator\Model\Schema> $schemas * + * @throws \Propel\Generator\Exception\BuildException + * * @return array */ - protected function buildVars(array $schemas): array + protected function buildDatabaseNameToTableMapDumps(array $schemas): array { - $databaseNameToTableMapNames = []; - + $entries = []; foreach ($schemas as $schema) { foreach ($schema->getDatabases(false) as $database) { $databaseName = $database->getName(); - $tableMapNames = array_map([$this, 'getFullyQualifiedTableMapClassName'], $database->getTables()); - if (array_key_exists($databaseName, $databaseNameToTableMapNames)) { - $existing = $databaseNameToTableMapNames[$databaseName]; - $tableMapNames = array_merge($existing, $tableMapNames); + if (!$databaseName) { + throw new BuildException('Cannot build table map of unnamed database'); } - sort($tableMapNames); - $databaseNameToTableMapNames[$databaseName] = $tableMapNames; + $tableMapDumps = $this->buildDatabaseMap($database)->dumpMaps(); + $entries[] = [$databaseName => $tableMapDumps]; } } + $databaseNameToTableMapDumps = array_merge_recursive(...$entries); + $this->sortRecursive($databaseNameToTableMapDumps); - return [ - 'databaseNameToTableMapNames' => $databaseNameToTableMapNames, - ]; + return $databaseNameToTableMapDumps; } /** - * @param \Propel\Generator\Model\Table $table + * @param \Propel\Generator\Model\Database $database * - * @return string + * @return \Propel\Runtime\Map\DatabaseMap */ - protected function getFullyQualifiedTableMapClassName(Table $table): string + protected function buildDatabaseMap(Database $database): DatabaseMap { - $builder = new TableMapBuilder($table); - $builder->setGeneratorConfig($this->generatorConfig); + $databaseName = $database->getName(); + $databaseMap = new DatabaseMap($databaseName); + foreach ($database->getTables() as $table) { + $tableName = $table->getName(); + $phpName = $table->getPhpName(); + $tableMapClass = $this->getFullyQualifiedTableMapClassName($table); + $databaseMap->registerTableMapClassByName($tableName, $phpName, $tableMapClass); + } - return $builder->getFullyQualifiedClassName(); + return $databaseMap; } /** - * @param array $vars + * @param array $array * - * @return string + * @return void */ - protected function renderTemplate(array $vars): string + protected function sortRecursive(array &$array): void { - $templatePath = $this->getTemplatePath(__DIR__); + ksort($array, SORT_STRING); + array_walk($array, fn (&$value) => is_array($value) && $this->sortRecursive($value)); + } - $filePath = $templatePath . 'tableMapLoaderScript.php'; - $template = new PropelTemplate(); - $template->setTemplateFile($filePath); + /** + * @param \Propel\Generator\Model\Table $table + * + * @return string + */ + protected function getFullyQualifiedTableMapClassName(Table $table): string + { + $builder = new TableMapBuilder($table); + $builder->setGeneratorConfig($this->generatorConfig); - return $template->render($vars); + return $builder->getFullyQualifiedClassName(); } /** diff --git a/src/Propel/Generator/Manager/AbstractManager.php b/src/Propel/Generator/Manager/AbstractManager.php index e03f07f974..9bb723df96 100644 --- a/src/Propel/Generator/Manager/AbstractManager.php +++ b/src/Propel/Generator/Manager/AbstractManager.php @@ -17,7 +17,7 @@ use Propel\Generator\Exception\EngineException; use Propel\Generator\Model\Database; use Propel\Generator\Model\Schema; -use XsltProcessor; +use XSLTProcessor; /** * An abstract base Propel manager to perform work related to the XML schema @@ -323,7 +323,7 @@ protected function loadDataModels(): void // normalize the document using normalizer stylesheet $xslDom = new DOMDocument('1.0', 'UTF-8'); $xslDom->load($this->xsl->getAbsolutePath()); - $xsl = new XsltProcessor(); + $xsl = new XSLTProcessor(); $xsl->importStyleSheet($xslDom); $dom = $xsl->transformToDoc($dom); } diff --git a/src/Propel/Generator/Model/Table.php b/src/Propel/Generator/Model/Table.php index b023f3f44e..0b29d5472a 100644 --- a/src/Propel/Generator/Model/Table.php +++ b/src/Propel/Generator/Model/Table.php @@ -1,13 +1,13 @@ (Propel) * @author John D. McNally (Torque) * @author Daniel Rall (Torque) + * + * @psalm-consistent-constructor (instantiated by class name in StandardServiceContainer without arguments) + * + * @psalm-type \Propel\Runtime\Map\MapType = 'tablesByName' | 'tablesByPhpName' + * @psalm-type \Propel\Runtime\Map\TableMapDump array<\Propel\Runtime\Map\MapType, array>> */ class DatabaseMap { @@ -38,14 +43,14 @@ class DatabaseMap /** * Tables in the database, using table name as key * - * @var array<\Propel\Runtime\Map\TableMap|class-string<\Propel\Runtime\Map\TableMap>> + * @var array> */ protected $tables = []; /** * Tables in the database, using table phpName as key * - * @var array<\Propel\Runtime\Map\TableMap|class-string<\Propel\Runtime\Map\TableMap>> + * @var array> */ protected $tablesByPhpName = []; @@ -121,7 +126,7 @@ protected function addTableByPhpName(?string $phpName, $tableOrClassMap): void /** * Add a new table to the database, using the tablemap class name. * - * @param string $tableMapClass The name of the table map to add + * @param class-string<\Propel\Runtime\Map\TableMap> $tableMapClass The name of the table map to add * * @return \Propel\Runtime\Map\TableMap The TableMap object */ @@ -134,6 +139,41 @@ public function addTableFromMapClass(string $tableMapClass): TableMap return $this->getTable($table->getName()); } + /** + * Dump table maps. Used during configuration generation. + * + * @psalm-return \Propel\Runtime\Map\TableMapDump + * + * @return array>> A dump that can be loaded again with {@link DatabaseMap::loadMapsFromDump()} + */ + public function dumpMaps(): array + { + /** + * @psalm-var \Closure( class-string<\Propel\Runtime\Map\TableMap>|\Propel\Runtime\Map\TableMap ): class-string<\Propel\Runtime\Map\TableMap> + */ + $toClassString = fn ($tableMap) => is_string($tableMap) ? $tableMap : get_class($tableMap); + + return [ + 'tablesByName' => array_map($toClassString, $this->tables), + 'tablesByPhpName' => array_map($toClassString, $this->tablesByPhpName), + ]; + } + + /** + * Load internal table maps from dump. Used during Propel initialization. + * + * @psalm-param \Propel\Runtime\Map\TableMapDump $mapsDump + * + * @param array>> $mapsDump Table map dump as created by {@link DatabaseMap::dumpMaps()} + * + * @return void + */ + public function loadMapsFromDump(array $mapsDump): void + { + $this->tables = $mapsDump['tablesByName']; + $this->tablesByPhpName = $mapsDump['tablesByPhpName']; + } + /** * Registers a table map classes (by qualified name) as table belonging * to this database. @@ -150,9 +190,28 @@ public function addTableFromMapClass(string $tableMapClass): TableMap public function registerTableMapClass(string $tableMapClass): void { $tableName = $tableMapClass::TABLE_NAME; - $this->tables[$tableName] = $tableMapClass; - $tablePhpName = $tableMapClass::TABLE_PHP_NAME; + $this->registerTableMapClassByName($tableName, $tablePhpName, $tableMapClass); + } + + /** + * Registers a table map classes (by qualified name) as table belonging + * to this database. + * + * Classes added like this will only be instantiated when accessed + * through {@link DatabaseMap::getTable()}, + * {@link DatabaseMap::getTableByPhpName()}, or + * {@link DatabaseMap::getTables()} + * + * @param string $tableName Internal name of the table, i.e. 'bookstore_schemas.book' + * @param string|null $tablePhpName PHP name of the table, i.e. 'Book' + * @param class-string<\Propel\Runtime\Map\TableMap> $tableMapClass The name of the table map to add + * + * @return void + */ + public function registerTableMapClassByName(string $tableName, ?string $tablePhpName, string $tableMapClass): void + { + $this->tables[$tableName] = $tableMapClass; $this->addTableByPhpName($tablePhpName, $tableMapClass); } @@ -160,7 +219,7 @@ public function registerTableMapClass(string $tableMapClass): void * Registers a list of table map classes (by qualified name) as table maps * belonging to this database. * - * @param array $tableMapClasses + * @param array> $tableMapClasses * * @return void */ diff --git a/src/Propel/Runtime/ServiceContainer/ServiceContainerInterface.php b/src/Propel/Runtime/ServiceContainer/ServiceContainerInterface.php index 6b43b71cbe..08da141aa6 100644 --- a/src/Propel/Runtime/ServiceContainer/ServiceContainerInterface.php +++ b/src/Propel/Runtime/ServiceContainer/ServiceContainerInterface.php @@ -36,7 +36,7 @@ interface ServiceContainerInterface * * @var string */ - public const DEFAULT_DATABASE_MAP_CLASS = '\Propel\Runtime\Map\DatabaseMap'; + public const DEFAULT_DATABASE_MAP_CLASS = DatabaseMap::class; /** * The name of the default datasource. @@ -50,7 +50,7 @@ interface ServiceContainerInterface * * @var string */ - public const DEFAULT_PROFILER_CLASS = '\Propel\Runtime\Util\Profiler'; + public const DEFAULT_PROFILER_CLASS = Profiler::class; /** * @return string diff --git a/src/Propel/Runtime/ServiceContainer/StandardServiceContainer.php b/src/Propel/Runtime/ServiceContainer/StandardServiceContainer.php index d9b6a15ccd..93e93970af 100644 --- a/src/Propel/Runtime/ServiceContainer/StandardServiceContainer.php +++ b/src/Propel/Runtime/ServiceContainer/StandardServiceContainer.php @@ -28,6 +28,9 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +/** + * @psalm-import-type \Propel\Runtime\Map\TableMapDump from \Propel\Runtime\Map\DatabaseMap + */ class StandardServiceContainer implements ServiceContainerInterface { /** @@ -55,7 +58,7 @@ class StandardServiceContainer implements ServiceContainerInterface protected $adapters = []; /** - * @phpstan-var array + * @phpstan-var array> * * @var array List of database adapter classes */ @@ -151,7 +154,7 @@ public function getAdapterClass(?string $name = null): string * This allows for lazy-loading adapter objects in getAdapter(). * * @param string $name The datasource name - * @param string $adapterClass + * @param class-string<\Propel\Runtime\Adapter\AdapterInterface> $adapterClass * * @return void */ @@ -164,7 +167,7 @@ public function setAdapterClass(string $name, string $adapterClass): void /** * Reset existing adapters classes and set new classes for all datasources. * - * @param array $adapterClasses A list of adapters + * @param array> $adapterClasses A list of adapters * * @return void */ @@ -270,6 +273,25 @@ public function initDatabaseMaps(array $databaseNameToTableMapClassNames = []): } } + /** + * @psalm-param array $databaseNameToTableMapDumps + * + * @param array>> $databaseNameToTableMapDumps + * + * @return void + */ + public function initDatabaseMapFromDumps(array $databaseNameToTableMapDumps = []): void + { + if ($this->databaseMaps === null) { + $this->databaseMaps = []; + } + + foreach ($databaseNameToTableMapDumps as $databaseName => $tableMapDumps) { + $databaseMap = $this->getDatabaseMap($databaseName); + $databaseMap->loadMapsFromDump($tableMapDumps); + } + } + /** * @phpstan-param class-string<\Propel\Runtime\Map\DatabaseMap> $databaseMapClass * @@ -587,7 +609,7 @@ protected function buildLogger(string $name = 'defaultLogger'): LoggerInterface $handler = new RotatingFileHandler( $configuration['path'], $configuration['max_files'] ?? 0, - $configuration['level'] ?? null, + $configuration['level'] ?? 100, $configuration['bubble'] ?? true, ); @@ -595,8 +617,8 @@ protected function buildLogger(string $name = 'defaultLogger'): LoggerInterface case 'syslog': $handler = new SyslogHandler( $configuration['ident'], - $configuration['facility'] ?? null, - $configuration['level'] ?? null, + $configuration['facility'] ?? LOG_USER, + $configuration['level'] ?? 100, $configuration['bubble'] ?? true, ); diff --git a/src/Propel/Runtime/Util/Profiler.php b/src/Propel/Runtime/Util/Profiler.php index 1380274e05..4df17fb721 100644 --- a/src/Propel/Runtime/Util/Profiler.php +++ b/src/Propel/Runtime/Util/Profiler.php @@ -12,6 +12,8 @@ /** * Profiler for Propel + * + * @psalm-consistent-constructor (instantiated by class name in StandardServiceContainer without arguments) */ class Profiler { diff --git a/templates/Builder/Om/tableMapLoaderScript.php b/templates/Builder/Om/tableMapLoaderScript.php index fe42ca954c..aa5dd4b83e 100644 --- a/templates/Builder/Om/tableMapLoaderScript.php +++ b/templates/Builder/Om/tableMapLoaderScript.php @@ -1,4 +1,4 @@ $serviceContainer = \Propel\Runtime\Propel::getServiceContainer(); -$serviceContainer->initDatabaseMaps(); +$serviceContainer->initDatabaseMapFromDumps(); diff --git a/tests/Propel/Tests/Runtime/Map/DatabaseMapTest.php b/tests/Propel/Tests/Runtime/Map/DatabaseMapTest.php index 126a3268b2..79ebbf5677 100644 --- a/tests/Propel/Tests/Runtime/Map/DatabaseMapTest.php +++ b/tests/Propel/Tests/Runtime/Map/DatabaseMapTest.php @@ -101,27 +101,39 @@ public function testAddTableFromMapClass() } } + /** + * @return void + */ + public function testRegisterTableMapClassByName() + { + $databaseMap = new DatabaseMapWithGetters('dummyDatabase'); + $tableName = 'database.table_name'; + $phpTableName = '\\php\\table\\name'; + $tableMapClass = 'TableClass'; + $databaseMap->registerTableMapClassByName($tableName, $phpTableName, $tableMapClass); + + $this->assertTableMapContains($databaseMap, $tableName, $phpTableName, $tableMapClass); + } + /** * @return void */ public function testRegisterTableByMapClassAddsClassAsString() { - $databaseMap = new class('dummyDatabase') extends DatabaseMap { - public function getRawTables(){ - return $this->tables; - } - - public function getRawTablesByPhpName(){ - return $this->tablesByPhpName; - } - }; + $databaseMap = new DatabaseMapWithGetters('dummyDatabase'); $tableMapClass = BazTableMap::class; $databaseMap->registerTableMapClass($tableMapClass); + $this->assertTableMapContains($databaseMap, $tableMapClass::TABLE_NAME, '\\' . $tableMapClass::TABLE_PHP_NAME, $tableMapClass); + } + + protected function assertTableMapContains(DatabaseMapWithGetters $databaseMap, $tableName, $phpTableName, $tableMapClass) + { + $tableNameToArray = [ - BazTableMap::TABLE_NAME => $databaseMap->getRawTables(), - '\\' . BazTableMap::TABLE_PHP_NAME => $databaseMap->getRawTablesByPhpName(), + $tableName => $databaseMap->getTablesByNameMap(), + $phpTableName => $databaseMap->getTablesByPhpNameMap(), ]; foreach($tableNameToArray as $name => $tables){ @@ -223,6 +235,24 @@ public function testGetTableByPhpNameNotLoaded() { $this->assertEquals('book', Propel::getServiceContainer()->getDatabaseMap('bookstore')->getTableByPhpName('Propel\Tests\Bookstore\Book')->getName(), 'getTableByPhpName() can autoload a TableMap when the class is generated and autoloaded'); } + + /** + * @return void + */ + public function testLoadMapsFromDump() + { + $sourceMap = new DatabaseMapWithGetters('source'); + $sourceMap->registerTableMapClass(BazTableMap::class); + $sourceMap->registerTableMapClassByName('le name', 'le php name', 'le quallfied class name'); + + $mapDump = $sourceMap->dumpMaps(); + + $targetMap = new DatabaseMapWithGetters('target'); + $targetMap->loadMapsFromDump($mapDump); + + $this->assertEquals($sourceMap->getTablesByNameMap(), $targetMap->getTablesByNameMap(), 'Name map should match'); + $this->assertEquals($sourceMap->getTablesByPhpNameMap(), $targetMap->getTablesByPhpNameMap(), 'PHP name map should match'); + } } class TestDatabaseBuilder @@ -268,3 +298,12 @@ public function initialize(): void $this->setPhpName(self::TABLE_PHP_NAME); } } + +class DatabaseMapWithGetters extends DatabaseMap{ + public function getTablesByNameMap(){ + return $this->tables; + } + public function getTablesByPhpNameMap(){ + return $this->tablesByPhpName; + } +}