diff --git a/config/database-env.php b/config/database-env.php index 589ba63..1e7d3aa 100644 --- a/config/database-env.php +++ b/config/database-env.php @@ -6,84 +6,88 @@ * Priority: Environment variables > User settings > Defaults */ - /** - * Get database connection data with proper precedence - * @return array Complete database configuration - */ - function getDatabaseConnectionData(): array { - // First check for DSN (takes precedence over everything) - if (!empty($_ENV["DSN"])) { - return parseDsn($_ENV["DSN"]); - } - - // Include database.php to get user settings merged with environment variables - $databaseFile = __DIR__ . '/database.php'; - - if (file_exists($databaseFile)) { - // Return the credential array from database.php - $databaseConfig = include $databaseFile; - return is_array($databaseConfig) ? $databaseConfig : []; + if (!function_exists('getDatabaseConnectionData')) { + /** + * Get database connection data with proper precedence + * @return array Complete database configuration + */ + function getDatabaseConnectionData(): array { + // First check for DSN (takes precedence over everything) + if (!empty($_ENV["DSN"])) { + return parseDsn($_ENV["DSN"]); + } + + // Include database.php to get user settings merged with environment variables + $databaseFile = __DIR__ . '/database.php'; + + if (file_exists($databaseFile)) { + // Return the credential array from database.php + $databaseConfig = include $databaseFile; + return is_array($databaseConfig) ? $databaseConfig : []; + } + + // Fallback if database.php doesn't exist - use only environment variables + return array_filter([ + 'driver' => $_ENV['DB_DRIVER'] ?? null, + 'host' => $_ENV['DB_HOST'] ?? null, + 'database' => $_ENV['DB_DATABASE'] ?? $_ENV['DB_NAME'] ?? null, + 'username' => $_ENV['DB_USERNAME'] ?? $_ENV['DB_USER'] ?? null, + 'password' => $_ENV['DB_PASSWORD'] ?? $_ENV['DB_PASS'] ?? null, + 'port' => isset($_ENV['DB_PORT']) ? (int)$_ENV['DB_PORT'] : null, + 'charset' => $_ENV['DB_CHARSET'] ?? null, + 'collation'=> $_ENV['DB_COLLATION'] ?? null, + ], fn($value) => $value !== null); } - - // Fallback if database.php doesn't exist - use only environment variables - return array_filter([ - 'driver' => $_ENV['DB_DRIVER'] ?? null, - 'host' => $_ENV['DB_HOST'] ?? null, - 'database' => $_ENV['DB_DATABASE'] ?? $_ENV['DB_NAME'] ?? null, - 'username' => $_ENV['DB_USERNAME'] ?? $_ENV['DB_USER'] ?? null, - 'password' => $_ENV['DB_PASSWORD'] ?? $_ENV['DB_PASS'] ?? null, - 'port' => isset($_ENV['DB_PORT']) ? (int)$_ENV['DB_PORT'] : null, - 'charset' => $_ENV['DB_CHARSET'] ?? null, - 'collation'=> $_ENV['DB_COLLATION'] ?? null, - ], fn($value) => $value !== null); } - /** - * Parse a DSN string into an array of database connection parameters - * @param string $dsn The DSN string to parse - * @return array Array of connection parameters - * @throws InvalidArgumentException If the DSN format is invalid - */ - function parseDsn(string $dsn): array { - // Parse the DSN - $parsed = parse_url($dsn); - - // Return nothing if the url can't be parsed - if ($parsed === false) { - return []; - } - - // Build a result array directly from parsed components - $result = array_filter([ - 'driver' => $parsed['scheme'] ?? null, - 'host' => $parsed['host'] ?? null, - 'database' => isset($parsed['path']) ? ltrim($parsed['path'], '/') : null, - 'username' => $parsed['user'] ?? null, - 'password' => $parsed['pass'] ?? null, - 'port' => $parsed['port'] ?? null, - ], fn($value) => $value !== null); - - // Handle query parameters efficiently - if (isset($parsed['query'])) { - parse_str($parsed['query'], $queryParams); + if (!function_exists('parseDsn')) { + /** + * Parse a DSN string into an array of database connection parameters + * @param string $dsn The DSN string to parse + * @return array Array of connection parameters + * @throws InvalidArgumentException If the DSN format is invalid + */ + function parseDsn(string $dsn): array { + // Parse the DSN + $parsed = parse_url($dsn); - // Extract encoding (charset takes precedence) - $encoding = $queryParams['charset'] ?? $queryParams['encoding'] ?? null; - - if ($encoding) { - $result['encoding'] = $encoding; + // Return nothing if the url can't be parsed + if ($parsed === false) { + return []; } - // Extract remaining flags - $flags = array_diff_key($queryParams, array_flip(['charset', 'encoding'])); + // Build a result array directly from parsed components + $result = array_filter([ + 'driver' => $parsed['scheme'] ?? null, + 'host' => $parsed['host'] ?? null, + 'database' => isset($parsed['path']) ? ltrim($parsed['path'], '/') : null, + 'username' => $parsed['user'] ?? null, + 'password' => $parsed['pass'] ?? null, + 'port' => $parsed['port'] ?? null, + ], fn($value) => $value !== null); - if ($flags) { - $result['flags'] = $flags; + // Handle query parameters efficiently + if (isset($parsed['query'])) { + parse_str($parsed['query'], $queryParams); + + // Extract encoding (charset takes precedence) + $encoding = $queryParams['charset'] ?? $queryParams['encoding'] ?? null; + + if ($encoding) { + $result['encoding'] = $encoding; + } + + // Extract remaining flags + $flags = array_diff_key($queryParams, array_flip(['charset', 'encoding'])); + + if ($flags) { + $result['flags'] = $flags; + } } + + return $result; } - - return $result; } - + // Return the database connection data return getDatabaseConnectionData(); \ No newline at end of file diff --git a/src/Collections/Collection.php b/src/Collections/Collection.php index be62009..3f8bfac 100644 --- a/src/Collections/Collection.php +++ b/src/Collections/Collection.php @@ -3,7 +3,7 @@ namespace Quellabs\ObjectQuel\Collections; /** - * A generic collection class + * A generic collection class with improved type safety and performance * @template T of object * @implements CollectionInterface */ @@ -16,16 +16,16 @@ class Collection implements CollectionInterface { protected array $collection; /** - * An array of sorted keys, if present. + * An array of sorted keys, cached for performance. * @var array|null */ protected ?array $sortedKeys = null; /** * Current position in the iteration of the collection. - * @var int|null + * @var int */ - protected ?int $position; + protected int $position = 0; /** * Indicates the sort order as a string. @@ -44,171 +44,29 @@ class Collection implements CollectionInterface { * @param string $sortOrder The sort order for the collection, default is an empty string. */ public function __construct(string $sortOrder = '') { - $this->collection = []; // Initialization of the collection array - $this->sortOrder = $sortOrder; // Initialization of the sort order - $this->position = null; // Initialization of the position - $this->isDirty = false; // The collection is not yet marked as modified - } - - /** - * Sort callback based on the sortOrder string - * This function is used to compare two elements of the collection - * @param mixed $a The first element to compare - * @param mixed $b The second element to compare - * @return int An integer indicating whether $a is less than, equal to, or greater than $b - */ - protected function sortCallback(mixed $a, mixed $b): int { - try { - $fields = array_map('trim', explode(',', $this->sortOrder)); - - foreach ($fields as $field) { - // Split each field into property and direction - // For example, "name ASC" becomes ["name", "ASC"] - $parts = array_map('trim', explode(' ', $field)); - $property = $parts[0]; - - // Determine the sort direction: -1 for DESC, 1 for ASC (default) - $direction = isset($parts[1]) && strtolower($parts[1]) === 'desc' ? -1 : 1; - - // Get the values for comparison - $valueA = $this->extractValue($a, $property); - $valueB = $this->extractValue($b, $property); - - // If both values are null, continue to the next field - if ($valueA === null && $valueB === null) { - continue; - } - - // Null values are considered larger in PHP - if ($valueA === null) { - return $direction; - } - - if ($valueB === null) { - return -$direction; - } - - // If both values are strings, use case-insensitive comparison - if (is_string($valueA) && is_string($valueB)) { - $result = strcasecmp($valueA, $valueB); - - if ($result > 0) { - return $direction; - } - - if ($result < 0) { - return -$direction; - } - } elseif ($valueA > $valueB) { - return $direction; - } elseif ($valueA < $valueB) { - return -$direction; - } - - // If the values are equal, continue to the next field - } - } catch (\ReflectionException $e) { - // Log any reflection errors - error_log("Reflection error in collection sort"); - } - - // If all fields are equal, maintain the original order - return 0; - } - - /** - * Extract a value from a variable based on the given property - * @param mixed $var The variable to extract the value from - * @param string $property The name of the property to extract - * @return mixed The extracted value, or null if not found - */ - protected function extractValue(mixed $var, string $property): mixed { - // If $var is an array, try to get the value with the property as key - if (is_array($var)) { - return $var[$property] ?? null; - } - - // If $var is an object, try to get the value in different ways - if (is_object($var)) { - // Check for a getter method (e.g. getName() for property 'name') - if (method_exists($var, 'get' . ucfirst($property))) { - return $var->{'get' . ucfirst($property)}(); - } - - // Use reflection to access private/protected properties - try { - $reflection = new \ReflectionClass($var); - - if ($reflection->hasProperty($property)) { - $prop = $reflection->getProperty($property); - $prop->setAccessible(true); - return $prop->getValue($var); - } - } catch (\ReflectionException $e) { - // Log the error if reflection fails - error_log("Reflection error in collection sort: " . $e->getMessage()); - } - } - - // For scalar values (int, float, string, bool), if - // the property is 'value', return the value itself. - if ($property === 'value' && is_scalar($var)) { - return $var; - } - - // If none of the above methods work, return null - return null; - } - - /** - * Calculate and sort the keys if needed. - * @return void - */ - protected function calculateSortedKeys(): void { - // Check if the data hasn't changed and the keys are already calculated - if (!$this->isDirty && $this->sortedKeys !== null) { - return; // Nothing to do, early return - } - - // Get the keys - $this->sortedKeys = $this->getKeys(); - - // Sort the keys if a sort order is set - if (!empty($this->sortOrder)) { - usort($this->sortedKeys, function($keyA, $keyB) { - return $this->sortCallback($this->collection[$keyA], $this->collection[$keyB]); - }); - } - - // Mark the keys as up-to-date + $this->collection = []; + $this->sortOrder = $sortOrder; + $this->position = 0; // Initialize to 0 instead of null $this->isDirty = false; } - /** - * Get the sorted keys of the collection - * @return array - */ - protected function getSortedKeys(): array { - $this->calculateSortedKeys(); - return $this->sortedKeys; - } - /** * Removes all entries from the collection * @return void */ public function clear(): void { $this->collection = []; - $this->position = null; + $this->position = 0; // Reset to 0 instead of null + $this->markDirty(); } /** * Returns true if the given key exists in the collection, false if not - * @param string $key + * @param string|int $key * @return bool */ - public function containsKey(string $key): bool { - return isset($this->collection[$key]); + public function containsKey(string|int $key): bool { + return array_key_exists($key, $this->collection); } /** @@ -241,10 +99,6 @@ public function getCount(): int { * @return T|null */ public function current() { - if ($this->position === null) { - return null; - } - $keys = $this->getSortedKeys(); if (!isset($keys[$this->position])) { @@ -273,9 +127,7 @@ public function first() { * @return void */ public function next(): void { - if ($this->position !== null) { - $this->position++; - } + $this->position++; } /** @@ -308,7 +160,7 @@ public function offsetSet(mixed $offset, mixed $value): void { $this->collection[$offset] = $value; } - $this->isDirty = true; + $this->markDirty(); } /** @@ -316,8 +168,10 @@ public function offsetSet(mixed $offset, mixed $value): void { * @param mixed $offset The key of the element to be removed. */ public function offsetUnset(mixed $offset): void { - unset($this->collection[$offset]); - $this->isDirty = true; + if (array_key_exists($offset, $this->collection)) { + unset($this->collection[$offset]); + $this->markDirty(); + } } /** @@ -325,10 +179,6 @@ public function offsetUnset(mixed $offset): void { * @return mixed The key of the current element, or null if the position is not valid. */ public function key(): mixed { - if ($this->position === null) { - return null; - } - $keys = $this->getSortedKeys(); return $keys[$this->position] ?? null; } @@ -338,10 +188,6 @@ public function key(): mixed { * @return bool True if the current position is valid, otherwise false. */ public function valid(): bool { - if ($this->position === null) { - return false; - } - $keys = $this->getSortedKeys(); return isset($keys[$this->position]); } @@ -352,7 +198,7 @@ public function valid(): bool { */ public function rewind(): void { $this->calculateSortedKeys(); - $this->position = empty($this->sortedKeys) ? null : 0; + $this->position = 0; // Always start at 0 } /** @@ -374,24 +220,24 @@ public function getKeys(): array { /** * Adds a new value to the collection * @param T $entity - * @return void + * @return void */ public function add($entity): void { $this->collection[] = $entity; - $this->isDirty = true; + $this->markDirty(); } /** * Removes a value from the collection * @param T $entity - * @return bool - */ + * @return bool + */ public function remove($entity): bool { $key = array_search($entity, $this->collection, true); if ($key !== false) { unset($this->collection[$key]); - $this->isDirty = true; + $this->markDirty(); return true; } @@ -404,26 +250,279 @@ public function remove($entity): bool { */ public function toArray(): array { $result = []; - + foreach ($this->getSortedKeys() as $key) { $result[] = $this->collection[$key]; } - + return $result; } /** - * Update the sort order - * @param string $sortOrder New sort order + * Filters the collection based on a callback function + * @param callable $callback A callback function that takes an element and returns bool + * @return array An array containing only elements that pass the filter */ - public function updateSortOrder(string $sortOrder): void { - // Save the new sort order - $this->sortOrder = $sortOrder; + public function filter(callable $callback): array { + $filtered = []; + + foreach ($this->collection as $key => $item) { + if ($callback($item, $key)) { + $filtered[] = $item; + } + } - // Reset sorted keys - $this->sortedKeys = null; + return $filtered; + } + + /** + * Maps each element of the collection through a callback function + * @param callable $callback A callback function that takes an element and returns a transformed value + * @return array An array containing the transformed elements + */ + public function map(callable $callback): array { + $mapped = []; - // Set dirty flag + foreach ($this->collection as $key => $item) { + $mapped[] = $callback($item, $key); + } + + return $mapped; + } + + /** + * Reduces the collection to a single value using a callback function + * @param callable $callback A callback function that takes accumulator, current item, and key + * @param mixed $initial The initial value for the accumulator + * @return mixed The final accumulated value + */ + public function reduce(callable $callback, mixed $initial = null): mixed { + $accumulator = $initial; + + foreach ($this->collection as $key => $item) { + $accumulator = $callback($accumulator, $item, $key); + } + + return $accumulator; + } + + /** + * Checks if any element in the collection matches the given callback + * @param callable $callback A callback function that takes an element and returns bool + * @return bool True if any element matches, false otherwise + */ + public function any(callable $callback): bool { + foreach ($this->collection as $key => $item) { + if ($callback($item, $key)) { + return true; + } + } + + return false; + } + + /** + * Marks the collection as modified and clears cache + */ + protected function markDirty(): void { $this->isDirty = true; + $this->sortedKeys = null; // Clear cache immediately for better performance + } + + /** + * Sort callback based on the sortOrder string + * This function is used to compare two elements of the collection + * @param T $a The first element to compare + * @param T $b The second element to compare + * @return int An integer indicating whether $a is less than, equal to, or greater than $b + */ + protected function sortCallback(mixed $a, mixed $b): int { + if (empty($this->sortOrder)) { + return 0; // Early return if no sort order + } + + try { + $fields = array_map('trim', explode(',', $this->sortOrder)); + + foreach ($fields as $field) { + if (empty($field)) continue; // Skip empty fields + + // Split each field into property and direction + // For example, "name ASC" becomes ["name", "ASC"] + $parts = array_map('trim', explode(' ', $field, 2)); // Limit to 2 parts + $property = $parts[0]; + + // Determine the sort direction: -1 for DESC, 1 for ASC (default) + $direction = isset($parts[1]) && strtolower($parts[1]) === 'desc' ? -1 : 1; + + // Get the values for comparison + $valueA = $this->extractValue($a, $property); + $valueB = $this->extractValue($b, $property); + + // If both values are null, continue to the next field + if ($valueA === null && $valueB === null) { + continue; + } + + // Handle null values consistently - null comes last in ASC order + if ($valueA === null) { + return $direction; + } + + if ($valueB === null) { + return -$direction; + } + + // Compare values with proper type handling + $result = $this->compareValues($valueA, $valueB); + if ($result !== 0) { + return $result * $direction; + } + + // If the values are equal, continue to the next field + } + } catch (\Exception $e) { + // Log any errors with more context + error_log("Collection sort error: " . $e->getMessage()); + } + + // If all fields are equal, maintain the original order + return 0; + } + + /** + * Compare two values with proper type handling + * @param mixed $a + * @param mixed $b + * @return int + */ + protected function compareValues(mixed $a, mixed $b): int { + // If both values are strings, use case-insensitive comparison + if (is_string($a) && is_string($b)) { + return strcasecmp($a, $b); + } + + // If both are numeric, compare numerically to avoid string comparison issues + if (is_numeric($a) && is_numeric($b)) { + return $a <=> $b; + } + + // If both are the same type, use spaceship operator + if (gettype($a) === gettype($b)) { + return $a <=> $b; + } + + // Fallback to string comparison for mixed types + return strcmp((string)$a, (string)$b); + } + + /** + * Extract a value from a variable based on the given property + * @param mixed $var The variable to extract the value from + * @param string $property The name of the property to extract + * @return mixed The extracted value, or null if not found + */ + protected function extractValue(mixed $var, string $property): mixed { + // If $var is an array, try to get the value with the property as key + if (is_array($var)) { + return $var[$property] ?? null; + } + + // For scalar values (int, float, string, bool), if + // the property is 'value', return the value itself. + if ($property === 'value' && is_scalar($var)) { + return $var; + } + + // If $var is an object, try to get the value in different ways + if (is_object($var)) { + // Check for a getter method (e.g. getName() for property 'name') - more efficient than reflection + $getter = 'get' . ucfirst($property); + + if (method_exists($var, $getter)) { + try { + return $var->$getter(); + } catch (\Exception $e) { + error_log("Getter method error for property '{$property}': " . $e->getMessage()); + // Continue to try other methods + } + } + + // Use reflection to access private/protected properties as last resort + try { + $reflection = new \ReflectionClass($var); + + if ($reflection->hasProperty($property)) { + // Fetch the property + $prop = $reflection->getProperty($property); + + // Mark the property to be accessible if it's private/protected + if (!$prop->isPublic()) { + $prop->setAccessible(true); + } + + // Check if property is initialized before getting value + if (!$prop->isInitialized($var)) { + // Return type-compatible default value for uninitialized typed properties + $type = $prop->getType(); + + if ($type instanceof \ReflectionNamedType && !$type->allowsNull()) { + return match ($type->getName()) { + 'string' => '', + 'int' => 0, + 'float' => 0.0, + 'bool' => false, + 'array' => [], + default => null + }; + } + + return null; + } + + // Return the value + return $prop->getValue($var); + } + } catch (\ReflectionException $e) { + // Log the error with more context + error_log("Reflection error for property '{$property}': " . $e->getMessage()); + } + } + + // If none of the above methods work, return null + return null; + } + + /** + * Calculate and sort the keys if needed with lazy evaluation. + * @return void + */ + protected function calculateSortedKeys(): void { + // Check if the data hasn't changed and the keys are already calculated + if (!$this->isDirty && $this->sortedKeys !== null) { + return; // Nothing to do, early return + } + + // Get the keys + $this->sortedKeys = $this->getKeys(); + + // Sort the keys if a sort order is set and we have items + if (!empty($this->sortOrder) && !empty($this->sortedKeys)) { + usort($this->sortedKeys, function($keyA, $keyB) { + return $this->sortCallback($this->collection[$keyA], $this->collection[$keyB]); + }); + } + + // Mark the keys as up-to-date + $this->isDirty = false; + } + + /** + * Get the sorted keys of the collection + * @return array + */ + protected function getSortedKeys(): array { + $this->calculateSortedKeys(); + return $this->sortedKeys ?? []; } } \ No newline at end of file diff --git a/src/Collections/EntityCollection.php b/src/Collections/EntityCollection.php index 2f373ae..83a4d63 100644 --- a/src/Collections/EntityCollection.php +++ b/src/Collections/EntityCollection.php @@ -310,4 +310,38 @@ public function toArray(): array { $this->doInitialize(); return $this->collection->toArray(); } + + /** + * Maps each element of the collection through a callback function + * @param callable $callback A callback function that takes an element and returns a transformed value + * @return array An array containing the transformed elements + * @throws QuelException + */ + public function map(callable $callback): array { + $this->doInitialize(); + return $this->collection->map($callback); + } + + /** + * Reduces the collection to a single value using a callback function + * @param callable $callback A callback function that takes accumulator, current item, and key + * @param mixed $initial The initial value for the accumulator + * @return mixed The final accumulated value + * @throws QuelException + */ + public function reduce(callable $callback, mixed $initial = null): mixed { + $this->doInitialize(); + return $this->collection->reduce($callback, $initial); + } + + /** + * Checks if any element in the collection matches the given callback + * @param callable $callback A callback function that takes an element and returns bool + * @return bool True if any element matches, false otherwise + * @throws QuelException + */ + public function any(callable $callback): bool { + $this->doInitialize(); + return $this->collection->any($callback); + } } \ No newline at end of file diff --git a/src/ReflectionManagement/PropertyHandler.php b/src/ReflectionManagement/PropertyHandler.php index 57fbb9b..213e18c 100644 --- a/src/ReflectionManagement/PropertyHandler.php +++ b/src/ReflectionManagement/PropertyHandler.php @@ -9,97 +9,16 @@ */ class PropertyHandler { - protected array $reflection_classes; - protected array $reflection_properties; - - /** - * PropertyHandler constructor. - */ - public function __construct() { - $this->reflection_properties = []; - $this->reflection_classes = []; - } - - /** - * Retrieves a ReflectionClass instance for the specified class. - * @param mixed $class The object or the name of the class to reflect. - * @return \ReflectionClass A ReflectionClass instance. - * @throws \ReflectionException - */ - private function getReflectionClass(mixed $class): \ReflectionClass { - // Determine the class name from the object or directly use the provided class name - $className = is_object($class) ? get_class($class) : $class; - - // Check if the ReflectionClass already exists in cache - if (!array_key_exists($className, $this->reflection_classes)) { - // Create a new ReflectionClass and cache it - $this->reflection_classes[$className] = new \ReflectionClass($className); - } - - // Return the cached or newly created ReflectionClass - return $this->reflection_classes[$className]; - } - - /** - * Retrieves the correct ReflectionProperty for a given property name in the class hierarchy. - * @param mixed $class The class name or object to inspect. - * @param string $propertyName The name of the property to search for. - * @return \ReflectionProperty|null The ReflectionProperty object if found, or null otherwise. - */ - private function getCorrectPropertyClass(mixed $class, string $propertyName): ?\ReflectionProperty { - try { - // Initialize ReflectionClass for the given class name or object - $reflectionClass = $this->getReflectionClass($class); - - // Loop through the class hierarchy until the property is found or until there are no more parent classes - do { - // Check if the current class in the hierarchy has the property - if ($reflectionClass->hasProperty($propertyName)) { - // If property exists, return the ReflectionProperty object - return $reflectionClass->getProperty($propertyName); - } - - // Move to the parent class for the next iteration - $reflectionClass = $reflectionClass->getParentClass(); - } while ($reflectionClass !== false); // Continue as long as there is a parent class - } catch (\ReflectionException $e) { - } - - // Return null if the property is not found in any class in the hierarchy - return null; - } - - /** - * Retrieves a ReflectionProperty instance for the specified property of a class. - * @param mixed $class The object or the name of the class to get the property from. - * @param string $propertyName The name of the property to reflect. - * @return \ReflectionProperty A ReflectionProperty instance. - */ - private function getReflectionProperty(mixed $class, string $propertyName): \ReflectionProperty { - // Determine the class name from the object or directly use the provided class name - $className = is_object($class) ? get_class($class) : $class; - - // Create a key based on the class name and property name - $key = "{$className}:{$propertyName}"; - - // Check if the ReflectionProperty already exists in cache - if (!array_key_exists($key, $this->reflection_properties)) { - // Create a new ReflectionProperty and make it accessible - $this->reflection_properties[$key] = $this->getCorrectPropertyClass($className, $propertyName); - $this->reflection_properties[$key]->setAccessible(true); - } - - // Return the cached or newly created ReflectionProperty - return $this->reflection_properties[$key]; - } + private array $reflection_classes = []; + private array $reflection_properties = []; /** * Returns true if the property exists, false if not - * @param mixed $objectOrClass + * @param string|object $objectOrClass * @param string $propertyName * @return bool */ - public function exists(mixed $objectOrClass, string $propertyName): bool { + public function exists(string|object $objectOrClass, string $propertyName): bool { try { $reflection = $this->getReflectionClass($objectOrClass); return $reflection->hasProperty($propertyName); @@ -110,11 +29,11 @@ public function exists(mixed $objectOrClass, string $propertyName): bool { /** * Gets a property value from an object using reflection - * @param object $object The object instance to get the property from + * @param string|object $object The object instance to get the property from * @param string $propertyName The name of the property to retrieve * @return mixed The property value if accessible, null if uninitialized, false on error */ - public function get($object, string $propertyName): mixed { + public function get(string|object $object, string $propertyName): mixed { try { // Get the reflection property object for the given property name $reflection = $this->getReflectionProperty($object, $propertyName); @@ -123,6 +42,20 @@ public function get($object, string $propertyName): mixed { // This prevents the "must not be accessed before initialization" Error // that occurs with typed properties in PHP 7.4+ if (!$reflection->isInitialized($object)) { + // Return type-compatible default value for uninitialized typed properties + $type = $reflection->getType(); + + if ($type instanceof \ReflectionNamedType && !$type->allowsNull()) { + return match ($type->getName()) { + 'string' => '', + 'int' => 0, + 'float' => 0.0, + 'bool' => false, + 'array' => [], + default => null + }; + } + return null; } @@ -137,12 +70,12 @@ public function get($object, string $propertyName): mixed { /** * Sets a property value - * @param $object + * @param string|object $object * @param string $propertyName * @param mixed $value * @return bool */ - public function set($object, string $propertyName, mixed $value): bool { + public function set(string|object $object, string $propertyName, mixed $value): bool { try { $reflection = $this->getReflectionProperty($object, $propertyName); $reflection->setValue($object, $value); @@ -151,4 +84,88 @@ public function set($object, string $propertyName, mixed $value): bool { return false; } } + + /** + * Retrieves a ReflectionClass instance for the specified class. + * @param string|object $class The object or the name of the class to reflect. + * @return \ReflectionClass A ReflectionClass instance. + * @throws \ReflectionException + */ + private function getReflectionClass(string|object $class): \ReflectionClass { + // Determine the class name from the object or directly use the provided class name + $className = is_object($class) ? get_class($class) : $class; + + // Check if the ReflectionClass already exists in cache + if (isset($this->reflection_classes[$className])) { + return $this->reflection_classes[$className]; + } + + // Create a new ReflectionClass + $reflection = new \ReflectionClass($className); + + // Cache it + $this->reflection_classes[$className] = $reflection; + + // Return the newly created ReflectionClass + return $reflection; + } + + /** + * Retrieves a ReflectionProperty instance for the specified property of a class. + * @param string|object $class The object or the name of the class to get the property from. + * @param string $propertyName The name of the property to reflect. + * @return \ReflectionProperty A ReflectionProperty instance. + * @throws \ReflectionException + */ + private function getReflectionProperty(string|object $class, string $propertyName): \ReflectionProperty { + // Determine the class name from the object or directly use the provided class name + $className = is_object($class) ? get_class($class) : $class; + + // Create a key based on the class name and property name + $key = "{$className}:{$propertyName}"; + + // Check if the ReflectionProperty already exists in cache + if (isset($this->reflection_properties[$key])) { + return $this->reflection_properties[$key]; + } + + // Create a new ReflectionProperty and make it accessible + $reflection = new \ReflectionProperty($className, $propertyName); + $reflection->setAccessible(true); + + // Cache it + $this->reflection_properties[$key] = $reflection; + + // Return newly created ReflectionProperty + return $reflection; + } + + /** + * Retrieves the correct ReflectionProperty for a given property name in the class hierarchy. + * @param string|object $class The class name or object to inspect. + * @param string $propertyName The name of the property to search for. + * @return \ReflectionProperty|null The ReflectionProperty object if found, or null otherwise. + */ + private function findPropertyInHierarchy(string|object $class, string $propertyName): ?\ReflectionProperty { + try { + // Initialize ReflectionClass for the given class name or object + $reflectionClass = $this->getReflectionClass($class); + + // Loop through the class hierarchy until the property is found or until there are no more parent classes + do { + // Check if the current class in the hierarchy has the property + if ($reflectionClass->hasProperty($propertyName)) { + // If property exists, return the ReflectionProperty object + return $reflectionClass->getProperty($propertyName); + } + + // Move to the parent class for the next iteration + $reflectionClass = $reflectionClass->getParentClass(); + } while ($reflectionClass !== false); // Continue as long as there is a parent class + } catch (\ReflectionException $e) { + } + + // Return null if the property is not found in any class in the hierarchy + return null; + } } \ No newline at end of file