1818use CodeIgniter \Events \Events ;
1919use CodeIgniter \I18n \Time ;
2020use Exception ;
21+ use ReflectionClass ;
22+ use ReflectionNamedType ;
23+ use ReflectionType ;
24+ use ReflectionUnionType ;
2125use stdClass ;
2226use Stringable ;
2327use Throwable ;
6064 */
6165abstract class BaseConnection implements ConnectionInterface
6266{
67+ /**
68+ * Cached builtin type names per class/property.
69+ *
70+ * @var array<class-string, array<string, list<string>>>
71+ */
72+ private static array $ propertyBuiltinTypesCache = [];
73+
6374 /**
6475 * Data Source Name / Connect string
6576 *
@@ -384,9 +395,14 @@ public function __construct(array $params)
384395 unset($ params ['dateFormat ' ]);
385396 }
386397
398+ $ typedPropertyTypes = $ this ->getBuiltinPropertyTypesMap (array_keys ($ params ));
399+
387400 foreach ($ params as $ key => $ value ) {
388401 if (property_exists ($ this , $ key )) {
389- $ this ->{$ key } = $ value ;
402+ $ this ->{$ key } = $ this ->castScalarValueForTypedProperty (
403+ $ value ,
404+ $ typedPropertyTypes [$ key ] ?? [],
405+ );
390406 }
391407 }
392408
@@ -404,6 +420,126 @@ public function __construct(array $params)
404420 }
405421 }
406422
423+ /**
424+ * Some config values (especially env overrides without clear source type)
425+ * can still reach us as strings. Coerce them for typed properties to keep
426+ * strict typing compatible.
427+ *
428+ * @param list<string> $types
429+ */
430+ private function castScalarValueForTypedProperty (mixed $ value , array $ types ): mixed
431+ {
432+ if (! is_string ($ value )) {
433+ return $ value ;
434+ }
435+
436+ if ($ types === [] || in_array ('string ' , $ types , true ) || in_array ('mixed ' , $ types , true )) {
437+ return $ value ;
438+ }
439+
440+ $ trimmedValue = trim ($ value );
441+
442+ if (in_array ('null ' , $ types , true ) && strtolower ($ trimmedValue ) === 'null ' ) {
443+ return null ;
444+ }
445+
446+ if (in_array ('int ' , $ types , true ) && preg_match ('/^[+-]?\d+$/ ' , $ trimmedValue ) === 1 ) {
447+ return (int ) $ trimmedValue ;
448+ }
449+
450+ if (in_array ('float ' , $ types , true ) && is_numeric ($ trimmedValue )) {
451+ return (float ) $ trimmedValue ;
452+ }
453+
454+ if (in_array ('bool ' , $ types , true ) || in_array ('false ' , $ types , true ) || in_array ('true ' , $ types , true )) {
455+ $ boolValue = filter_var ($ trimmedValue , FILTER_VALIDATE_BOOLEAN , FILTER_NULL_ON_FAILURE );
456+
457+ if ($ boolValue !== null ) {
458+ if (in_array ('bool ' , $ types , true )) {
459+ return $ boolValue ;
460+ }
461+
462+ if ($ boolValue === false && in_array ('false ' , $ types , true )) {
463+ return false ;
464+ }
465+
466+ if ($ boolValue === true && in_array ('true ' , $ types , true )) {
467+ return true ;
468+ }
469+ }
470+ }
471+
472+ return $ value ;
473+ }
474+
475+ /**
476+ * @param list<string> $properties
477+ *
478+ * @return array<string, list<string>>
479+ */
480+ private function getBuiltinPropertyTypesMap (array $ properties ): array
481+ {
482+ $ className = static ::class;
483+ $ requested = array_fill_keys ($ properties , true );
484+
485+ if (! isset (self ::$ propertyBuiltinTypesCache [$ className ])) {
486+ self ::$ propertyBuiltinTypesCache [$ className ] = [];
487+ }
488+
489+ // Fill only the properties requested by this call that are not cached yet.
490+ $ missing = array_diff_key ($ requested , self ::$ propertyBuiltinTypesCache [$ className ]);
491+
492+ if ($ missing !== []) {
493+ $ reflection = new ReflectionClass ($ className );
494+
495+ foreach ($ reflection ->getProperties () as $ property ) {
496+ $ propertyName = $ property ->getName ();
497+
498+ if (! isset ($ missing [$ propertyName ])) {
499+ continue ;
500+ }
501+
502+ $ type = $ property ->getType ();
503+
504+ if (! $ type instanceof ReflectionType) {
505+ self ::$ propertyBuiltinTypesCache [$ className ][$ propertyName ] = [];
506+
507+ continue ;
508+ }
509+
510+ $ namedTypes = $ type instanceof ReflectionUnionType ? $ type ->getTypes () : [$ type ];
511+ $ builtinTypes = [];
512+
513+ foreach ($ namedTypes as $ namedType ) {
514+ if (! $ namedType instanceof ReflectionNamedType || ! $ namedType ->isBuiltin ()) {
515+ continue ;
516+ }
517+
518+ $ builtinTypes [] = $ namedType ->getName ();
519+ }
520+
521+ if ($ type ->allowsNull () && ! in_array ('null ' , $ builtinTypes , true )) {
522+ $ builtinTypes [] = 'null ' ;
523+ }
524+
525+ self ::$ propertyBuiltinTypesCache [$ className ][$ propertyName ] = $ builtinTypes ;
526+ }
527+
528+ // Untyped or unresolved properties are cached as empty to avoid re-reflecting them.
529+ foreach (array_keys ($ missing ) as $ propertyName ) {
530+ self ::$ propertyBuiltinTypesCache [$ className ][$ propertyName ] ??= [];
531+ }
532+ }
533+
534+ $ typedProperties = [];
535+
536+ foreach ($ properties as $ property ) {
537+ $ typedProperties [$ property ] = self ::$ propertyBuiltinTypesCache [$ className ][$ property ] ?? [];
538+ }
539+
540+ return $ typedProperties ;
541+ }
542+
407543 /**
408544 * Initializes the database connection/settings.
409545 *
@@ -445,10 +581,15 @@ public function initialize()
445581 if (! empty ($ this ->failover ) && is_array ($ this ->failover )) {
446582 // Go over all the failovers
447583 foreach ($ this ->failover as $ index => $ failover ) {
584+ $ typedPropertyTypes = $ this ->getBuiltinPropertyTypesMap (array_keys ($ failover ));
585+
448586 // Replace the current settings with those of the failover
449587 foreach ($ failover as $ key => $ val ) {
450588 if (property_exists ($ this , $ key )) {
451- $ this ->{$ key } = $ val ;
589+ $ this ->{$ key } = $ this ->castScalarValueForTypedProperty (
590+ $ val ,
591+ $ typedPropertyTypes [$ key ] ?? [],
592+ );
452593 }
453594 }
454595
0 commit comments