From 8ec41c48d9efdc9f5b2e7d291029cc48e82db9a7 Mon Sep 17 00:00:00 2001 From: noescom Date: Thu, 5 Jun 2025 13:19:06 +0200 Subject: [PATCH 01/10] Added functions filter, map, reduce and any to Collection and EntityCollection --- src/Collections/Collection.php | 63 ++++++++++++++++++++++++++++ src/Collections/EntityCollection.php | 34 +++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/src/Collections/Collection.php b/src/Collections/Collection.php index be62009..7d26901 100644 --- a/src/Collections/Collection.php +++ b/src/Collections/Collection.php @@ -426,4 +426,67 @@ public function updateSortOrder(string $sortOrder): void { // Set dirty flag $this->isDirty = true; } + + /** + * 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 filter(callable $callback): array { + $filtered = []; + + foreach ($this->collection as $key => $item) { + if ($callback($item, $key)) { + $filtered[] = $item; + } + } + + 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 = []; + + 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; + } } \ 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 From ac82dd594667c5d0f2e2227b5771b73b04e85182 Mon Sep 17 00:00:00 2001 From: noescom Date: Thu, 5 Jun 2025 14:36:22 +0200 Subject: [PATCH 02/10] Collection now lazy sorts --- src/Collections/Collection.php | 418 ++++++++++++++++++--------------- 1 file changed, 233 insertions(+), 185 deletions(-) diff --git a/src/Collections/Collection.php b/src/Collections/Collection.php index 7d26901..fe027f4 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,11 +250,11 @@ public function remove($entity): bool { */ public function toArray(): array { $result = []; - + foreach ($this->getSortedKeys() as $key) { $result[] = $this->collection[$key]; } - + return $result; } @@ -420,11 +266,8 @@ public function updateSortOrder(string $sortOrder): void { // Save the new sort order $this->sortOrder = $sortOrder; - // Reset sorted keys - $this->sortedKeys = null; - - // Set dirty flag - $this->isDirty = true; + // Use markDirty() instead of manual setting + $this->markDirty(); } /** @@ -489,4 +332,209 @@ public function any(callable $callback): bool { 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 From 64ceb05f2128344705d261960b7336159ce4a17f Mon Sep 17 00:00:00 2001 From: noescom Date: Thu, 5 Jun 2025 14:45:09 +0200 Subject: [PATCH 03/10] get() now returns a type appropriate default value when the property is uninitialized and not nullable --- src/ReflectionManagement/PropertyHandler.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/ReflectionManagement/PropertyHandler.php b/src/ReflectionManagement/PropertyHandler.php index 57fbb9b..871b4bb 100644 --- a/src/ReflectionManagement/PropertyHandler.php +++ b/src/ReflectionManagement/PropertyHandler.php @@ -114,7 +114,7 @@ public function exists(mixed $objectOrClass, string $propertyName): bool { * @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(object $object, string $propertyName): mixed { try { // Get the reflection property object for the given property name $reflection = $this->getReflectionProperty($object, $propertyName); @@ -123,6 +123,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; } From 3a896c1e2ed39aa715c7ab869a8ffe392e5ce3b2 Mon Sep 17 00:00:00 2001 From: noescom Date: Thu, 5 Jun 2025 14:52:34 +0200 Subject: [PATCH 04/10] Updated mixed typehints to string|object --- src/ReflectionManagement/PropertyHandler.php | 158 +++++++++---------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/src/ReflectionManagement/PropertyHandler.php b/src/ReflectionManagement/PropertyHandler.php index 871b4bb..3832bf2 100644 --- a/src/ReflectionManagement/PropertyHandler.php +++ b/src/ReflectionManagement/PropertyHandler.php @@ -19,87 +19,14 @@ 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]; - } /** * 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 +37,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 $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); @@ -151,12 +78,12 @@ public function get(object $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); @@ -165,4 +92,77 @@ 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 (!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 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 getCorrectPropertyClass(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; + } + + /** + * 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. + */ + 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 (!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]; + } } \ No newline at end of file From a253258875de37bc3bb22ceae39f1fea86811ec1 Mon Sep 17 00:00:00 2001 From: noescom Date: Thu, 5 Jun 2025 15:05:21 +0200 Subject: [PATCH 05/10] Renamed getCorrectPropertyClass to findPropertyInHierarchy --- src/ReflectionManagement/PropertyHandler.php | 62 +++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/src/ReflectionManagement/PropertyHandler.php b/src/ReflectionManagement/PropertyHandler.php index 3832bf2..f5524ff 100644 --- a/src/ReflectionManagement/PropertyHandler.php +++ b/src/ReflectionManagement/PropertyHandler.php @@ -9,16 +9,8 @@ */ class PropertyHandler { - protected array $reflection_classes; - protected array $reflection_properties; - - /** - * PropertyHandler constructor. - */ - public function __construct() { - $this->reflection_properties = []; - $this->reflection_classes = []; - } + private array $reflection_classes = []; + private array $reflection_properties = []; /** * Returns true if the property exists, false if not @@ -112,6 +104,30 @@ private function getReflectionClass(string|object $class): \ReflectionClass { // Return the cached or newly created ReflectionClass return $this->reflection_classes[$className]; } + + /** + * 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. + */ + 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 (!array_key_exists($key, $this->reflection_properties)) { + // Create a new ReflectionProperty and make it accessible + $this->reflection_properties[$key] = $this->findPropertyInHierarchy($className, $propertyName); + $this->reflection_properties[$key]->setAccessible(true); + } + + // Return the cached or newly created ReflectionProperty + return $this->reflection_properties[$key]; + } /** * Retrieves the correct ReflectionProperty for a given property name in the class hierarchy. @@ -119,7 +135,7 @@ private function getReflectionClass(string|object $class): \ReflectionClass { * @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(string|object $class, string $propertyName): ?\ReflectionProperty { + private function findPropertyInHierarchy(string|object $class, string $propertyName): ?\ReflectionProperty { try { // Initialize ReflectionClass for the given class name or object $reflectionClass = $this->getReflectionClass($class); @@ -141,28 +157,4 @@ private function getCorrectPropertyClass(string|object $class, string $propertyN // 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 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. - */ - 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 (!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]; - } } \ No newline at end of file From 01a2d08a09afad2be4f239076827df0c76d23d6c Mon Sep 17 00:00:00 2001 From: noescom Date: Thu, 5 Jun 2025 15:09:52 +0200 Subject: [PATCH 06/10] Replaced array_key_exists with isset --- src/ReflectionManagement/PropertyHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ReflectionManagement/PropertyHandler.php b/src/ReflectionManagement/PropertyHandler.php index f5524ff..8dc809d 100644 --- a/src/ReflectionManagement/PropertyHandler.php +++ b/src/ReflectionManagement/PropertyHandler.php @@ -96,7 +96,7 @@ private function getReflectionClass(string|object $class): \ReflectionClass { $className = is_object($class) ? get_class($class) : $class; // Check if the ReflectionClass already exists in cache - if (!array_key_exists($className, $this->reflection_classes)) { + if (!isset($this->reflection_classes[$className])) { // Create a new ReflectionClass and cache it $this->reflection_classes[$className] = new \ReflectionClass($className); } @@ -119,7 +119,7 @@ private function getReflectionProperty(string|object $class, string $propertyNam $key = "{$className}:{$propertyName}"; // Check if the ReflectionProperty already exists in cache - if (!array_key_exists($key, $this->reflection_properties)) { + if (!isset($this->reflection_properties[$key])) { // Create a new ReflectionProperty and make it accessible $this->reflection_properties[$key] = $this->findPropertyInHierarchy($className, $propertyName); $this->reflection_properties[$key]->setAccessible(true); From 4b9164f7b3d72fc3f08b8118f0b05bfe3653afc0 Mon Sep 17 00:00:00 2001 From: noescom Date: Thu, 5 Jun 2025 16:04:04 +0200 Subject: [PATCH 07/10] Removed updateSortOrder --- src/Collections/Collection.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Collections/Collection.php b/src/Collections/Collection.php index fe027f4..3f8bfac 100644 --- a/src/Collections/Collection.php +++ b/src/Collections/Collection.php @@ -258,18 +258,6 @@ public function toArray(): array { return $result; } - /** - * Update the sort order - * @param string $sortOrder New sort order - */ - public function updateSortOrder(string $sortOrder): void { - // Save the new sort order - $this->sortOrder = $sortOrder; - - // Use markDirty() instead of manual setting - $this->markDirty(); - } - /** * Filters the collection based on a callback function * @param callable $callback A callback function that takes an element and returns bool From 7485c50e95e89cd5866544cb021c78206521989d Mon Sep 17 00:00:00 2001 From: noescom Date: Thu, 5 Jun 2025 18:39:24 +0200 Subject: [PATCH 08/10] Improved cache logic --- src/ReflectionManagement/PropertyHandler.php | 32 +++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/ReflectionManagement/PropertyHandler.php b/src/ReflectionManagement/PropertyHandler.php index 8dc809d..ce73261 100644 --- a/src/ReflectionManagement/PropertyHandler.php +++ b/src/ReflectionManagement/PropertyHandler.php @@ -96,13 +96,18 @@ private function getReflectionClass(string|object $class): \ReflectionClass { $className = is_object($class) ? get_class($class) : $class; // Check if the ReflectionClass already exists in cache - if (!isset($this->reflection_classes[$className])) { - // Create a new ReflectionClass and cache it - $this->reflection_classes[$className] = new \ReflectionClass($className); + if (isset($this->reflection_classes[$className])) { + return $this->reflection_classes[$className]; } - // Return the cached or newly created ReflectionClass - 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; } /** @@ -119,14 +124,19 @@ private function getReflectionProperty(string|object $class, string $propertyNam $key = "{$className}:{$propertyName}"; // Check if the ReflectionProperty already exists in cache - if (!isset($this->reflection_properties[$key])) { - // Create a new ReflectionProperty and make it accessible - $this->reflection_properties[$key] = $this->findPropertyInHierarchy($className, $propertyName); - $this->reflection_properties[$key]->setAccessible(true); + if (isset($this->reflection_properties[$key])) { + return $this->reflection_properties[$key]; } - // Return the cached or newly created ReflectionProperty - 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; } /** From 8e186e31fa6e508b6ea8c65578dd41ba196730cd Mon Sep 17 00:00:00 2001 From: noescom Date: Thu, 5 Jun 2025 18:51:55 +0200 Subject: [PATCH 09/10] Added throws annotation --- src/ReflectionManagement/PropertyHandler.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ReflectionManagement/PropertyHandler.php b/src/ReflectionManagement/PropertyHandler.php index ce73261..213e18c 100644 --- a/src/ReflectionManagement/PropertyHandler.php +++ b/src/ReflectionManagement/PropertyHandler.php @@ -109,12 +109,13 @@ private function getReflectionClass(string|object $class): \ReflectionClass { // 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 From 4f29425f66c501b8bc50c0f3163db490727241f0 Mon Sep 17 00:00:00 2001 From: noescom Date: Tue, 10 Jun 2025 17:12:29 +0200 Subject: [PATCH 10/10] Wrapped functions in database-env into function_exists clauses --- config/database-env.php | 142 +++++++++++++++++++++------------------- 1 file changed, 73 insertions(+), 69 deletions(-) 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