Skip to content

Commit 72ad3cf

Browse files
authored
Merge pull request #118 from Planetbiru/feature/3.14.6
Feature/3.14.6
2 parents 6ae38f1 + e4ca566 commit 72ad3cf

File tree

3 files changed

+91
-146
lines changed

3 files changed

+91
-146
lines changed

CHANGELOG.md

Lines changed: 21 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,140 +1034,40 @@ class UserValidator extends MagicObject
10341034
This enhancement makes the validator generator more expressive and future-proof, especially when building layered architectures or generating documentation automatically.
10351035

10361036

1037-
# MagicObject Version 3.14.4
1038-
1039-
## What's Fixed
1040-
1041-
### Bug Fix: `numberFormat*` Methods Accept Single Parameter
1042-
1043-
In version 3.14.4, a bug has been fixed in the internal magic method handling for `numberFormat*` methods (such as `numberFormatPercent`, `numberFormatTotal`, etc.).
1044-
1045-
#### Previous Behavior (Before 3.14.4)
1046-
1047-
Calling a `numberFormat*` method with **only one argument** would trigger warnings:
1048-
1049-
```php
1050-
$data = new MagicObject();
1051-
$data->setPercent(2.123456);
1052-
echo $data->numberFormatPercent(2);
1053-
```
1054-
1055-
**Result:**
1056-
1057-
```txt
1058-
Warning: Undefined index 1
1059-
Warning: Undefined index 2
1060-
```
1061-
1062-
This occurred because the internal handler expected three parameters and did not check for their existence properly.
1063-
1064-
#### New Behavior (Since 3.14.4)
1065-
1066-
These methods now safely accept **a single argument**, defaulting the missing parameters to reasonable values internally. The example above now works as expected and outputs:
1067-
1068-
```txt
1069-
2.12
1070-
```
1071-
1072-
#### Benefits:
1073-
1074-
- Improved developer experience when formatting numbers.
1075-
1076-
- No need to always pass three parameters for simple formatting.
1077-
1078-
- Prevents PHP warnings in production environments.
1079-
1080-
This bug fix enhances robustness and backward compatibility for developers using dynamic number formatting features in MagicObject.
10811037

10821038

10831039
# MagicObject Version 3.14.5
10841040

1085-
## What's Changed
1086-
1087-
### New Feature: URL-Based Database Credential Parsing
1088-
1089-
MagicObject now supports importing database credentials from a **datasource URL** string using the new method `PicoDatabaseCredentials::importFromUrl()`.
1090-
1091-
This enhancement simplifies configuration and integration with environment-based or externalized connection settings (e.g., `DATABASE_URL`).
1092-
1093-
#### Supported URL Format
1094-
1095-
driver://username:password@host:port/database?schema=public&charset=utf8&timezone=Asia/Jakarta
1041+
## Improvements
10961042

1043+
### Enhancement: Flexible Nested Retrieval in `retrieve()` Method
10971044

1098-
#### Example
1045+
The `retrieve(...$keys)` method now supports multiple input formats for accessing nested object properties:
10991046

1100-
```php
1101-
$credentials = new PicoDatabaseCredentials();
1102-
$credentials->importFromUrl(
1103-
'mysql://user:secret@localhost:3306/myapp?schema=public&charset=utf8mb4&timezone=Asia/Jakarta'
1104-
);
1105-
```
1106-
1107-
#### Optional Override
1108-
1109-
Username and password can be passed directly to override the values in the URL:
1110-
1111-
```php
1112-
$credentials->importFromUrl($url, 'realuser', 'realpass');
1113-
```
1114-
1115-
This is useful when credentials are stored separately from the connection string.
1116-
1117-
#### Special Handling for SQLite
1118-
1119-
When using sqlite:///path/to/database.db, the file path is automatically mapped to databaseFilePath instead of host/port.
1120-
1121-
```php
1122-
$url = 'sqlite:///path/to/database.db';
1123-
$credentials->importFromUrl($url);
1124-
```
1125-
1126-
### Why It Matters
1047+
- Dot notation: `$obj->retrieve('user.profile.name')`
1048+
- Arrow notation: `$obj->retrieve('user->profile->name')`
1049+
- Multiple arguments: `$obj->retrieve('user', 'profile', 'name')`
11271050

1128-
- Makes deployment and configuration more flexible in containerized or cloud environments.
1051+
Each key is automatically camelized for consistent property access.
1052+
If any key in the chain does not exist or returns `null`, the method will return `null`.
11291053

1130-
- Simplifies integration with .env files, environment variables, or external secrets managers.
1054+
This enhancement improves developer ergonomics when working with deeply nested data structures.
11311055

1132-
- Supports both traditional and SQLite-based databases.
1056+
### Validation: New `@TimeRange` Annotation
11331057

1134-
### Backward Compatibility
1135-
1136-
This update is fully backward-compatible and does not change any existing behavior unless the new method is used explicitly.
1137-
1138-
1139-
### Bug Fix: Class-Typed Default Parameters Now Compatible with PHP 5
1140-
1141-
Fixed a **fatal error** caused by the use of default parameters with a class type hint (`MagicObject`, `SecretObject`, `SetterGetter`, `MagicDto`) and non-null default values in the `validate()` method.
1142-
1143-
#### Before (Problematic in PHP 5):
1144-
1145-
```php
1146-
public function validate(
1147-
$parentPropertyName = null,
1148-
$messageTemplate = null,
1149-
MagicObject $reference = null,
1150-
bool $validateIfReferenceEmpty = true // ❌ Causes error in PHP 5
1151-
)
1152-
```
1153-
1154-
After (PHP 5 Compatible):
1058+
Added support for the `@TimeRange` validation annotation to validate time values within a specific range.
11551059

1060+
**Usage Example:**
11561061
```php
1157-
public function validate(
1158-
$parentPropertyName = null,
1159-
$messageTemplate = null,
1160-
MagicObject $reference = null,
1161-
$validateIfReferenceEmpty = true // ✅ Compatible with PHP 5
1162-
)
1163-
```
1062+
/**
1063+
* @TimeRange(from="08:00", to="17:00")
1064+
*/
1065+
public $jamKerja;
1066+
````
11641067

1165-
> This change ensures full compatibility with legacy environments running PHP 5, while maintaining functionality in modern PHP versions.
1068+
* Accepts time strings in `HH:MM` or `HH:MM:SS` format.
1069+
* Ensures the value falls within the defined range (inclusive).
1070+
* Useful for scheduling, availability windows, or working hours validation.
11661071

1167-
### Backward Compatibility
1072+
This new validation rule strengthens form-level data validation where time constraints are critical.
11681073

1169-
- This version is **fully backward-compatible**.
1170-
1171-
- No breaking changes were introduced.
1172-
1173-
- Existing codebases will continue to function as-is unless the new functionality is explicitly invoked.

src/MagicObject.php

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,46 +1297,53 @@ public function getOrDefault($propertyName, $defaultValue = null)
12971297
}
12981298

12991299
/**
1300-
* Retrieves a value from a nested object based on the provided keys.
1301-
* This function allows you to access nested properties of an object by passing the keys
1302-
* in a dot-notation-like fashion, where each key corresponds to a level in the object hierarchy.
1303-
*
1304-
* The method will return the value at the deepest level if all the keys exist. If any key does not
1305-
* exist or the value at any level is not set, the function will return `null`.
1306-
*
1307-
* @param string ...$keys The keys used to retrieve the value at various levels of the object.
1308-
* Each key corresponds to a property in the object, and the keys are automatically camelized.
1309-
*
1310-
* @return mixed|null The value found at the specified keys in the object, or `null` if any key is not found.
1300+
* Retrieves a value from a nested object using a series of keys.
1301+
*
1302+
* This method allows you to access deeply nested properties of an object by specifying
1303+
* the keys either as multiple arguments or as a single string using dot (`.`) or arrow (`->`) notation.
1304+
* Each key will be automatically converted to camelCase before property access.
1305+
*
1306+
* Supported usage examples:
1307+
* - $obj->retrieve('user', 'profile', 'name')
1308+
* - $obj->retrieve('user.profile.name')
1309+
* - $obj->retrieve('user->profile->name')
1310+
*
1311+
* If all keys exist and the values are properly set, the final nested value is returned.
1312+
* If any key is missing or the chain is broken at any point, the method returns `null`.
1313+
*
1314+
* @param string ...$keys One or more keys, either as multiple arguments or a single string with separators.
1315+
* @return mixed|null The final nested value, or `null` if any key is not found.
13111316
*/
13121317
public function retrieve(...$keys)
13131318
{
1314-
// Start from the current object
13151319
$currentData = $this;
1316-
1317-
// Loop through all provided keys
1320+
$normalizedKeys = [];
1321+
13181322
foreach ($keys as $key) {
1319-
// Convert key to camelCase format for consistency
1320-
if($key === null)
1321-
{
1322-
break;
1323+
if ($key === null) {
1324+
continue;
1325+
}
1326+
1327+
// Split string by common delimiters and add to normalized list
1328+
$parts = preg_split('/->|\./', $key);
1329+
foreach ($parts as $part) {
1330+
if (strlen(trim($part)) > 0) {
1331+
$normalizedKeys[] = PicoStringUtil::camelize($part);
1332+
}
13231333
}
1324-
$key = PicoStringUtil::camelize($key);
1334+
}
13251335

1326-
// Check if the current data object has the given key and the value is not null
1336+
foreach ($normalizedKeys as $key) {
13271337
if (isset($currentData) && $currentData instanceof self && $currentData->hasValue($key)) {
1328-
// If the key exists, get the value at this level
13291338
$currentData = $currentData->get($key);
13301339
} else {
1331-
// If any key is not found, return null
13321340
return null;
13331341
}
13341342
}
13351343

1336-
// Return the final value at the deepest level
13371344
return $currentData;
13381345
}
1339-
1346+
13401347
/**
13411348
* Set property value (magic setter).
13421349
*

src/Util/ValidationUtil.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,15 @@ public function init($customTemplates = array())
9393
'ip' => "Field '\${property}' must be a valid IP address",
9494
'dateFormat' => "Field '\${property}' must match the date format '\${format}'",
9595
'phone' => "Field '\${property}' must be a valid phone number",
96-
'enum' => "Field '\${property}' has an invalid value. Allowed values: \${allowedValues}.",
96+
'enum' => "Field '\${property}' has an invalid value. Allowed values: \${allowedValues}",
9797
'alpha' => "Field '\${property}' must contain only alphabetic characters",
9898
'alphaNumeric' => "Field '\${property}' must contain only alphanumeric characters",
9999
'startsWith' => "Field '\${property}' must start with '\${prefix}'",
100100
'endsWith' => "Field '\${property}' must end with '\${suffix}'",
101101
'contains' => "Field '\${property}' must contain '\${substring}'",
102102
'beforeDate' => "Field '\${property}' must be before '\${date}'",
103103
'afterDate' => "Field '\${property}' must be after '\${date}'",
104+
'timeRange' => "Field '\${property}' must be between \${min} and \${max}",
104105
);
105106

106107
// Process custom templates to camelize keys
@@ -225,6 +226,7 @@ public function validate($object, $parentPropertyName = null) // NOSONAR
225226
$this->validateContainsAnnotation($fullPropertyName, $propertyValue, $docComment);
226227
$this->validateBeforeDateAnnotation($fullPropertyName, $propertyValue, $docComment);
227228
$this->validateAfterDateAnnotation($fullPropertyName, $propertyValue, $docComment);
229+
$this->validateTimeRangeAnnotation($fullPropertyName, $propertyValue, $docComment);
228230
$this->validateSizeAnnotation($fullPropertyName, $propertyValue, $docComment);
229231
$this->validateMinAnnotation($fullPropertyName, $propertyValue, $docComment);
230232
$this->validateMaxAnnotation($fullPropertyName, $propertyValue, $docComment);
@@ -1371,4 +1373,40 @@ private function validateAfterDateAnnotation($propertyName, $propertyValue, $doc
13711373
}
13721374
}
13731375
}
1376+
1377+
/**
1378+
* Validates the **`TimeRange`** annotation.
1379+
* Ensures a time is within a specified range (min and max).
1380+
*/
1381+
private function validateTimeRangeAnnotation($propertyName, $propertyValue, $docComment)
1382+
{
1383+
if (preg_match('/@TimeRange\(([^)]*)\)/', $docComment, $matches)) {
1384+
$params = $this->parseAnnotationParams($matches[1]);
1385+
$min = isset($params['min']) ? $params['min'] : '00:00'; // Default min to start of day
1386+
$max = isset($params['max']) ? $params['max'] : '23:59'; // Default max to end of day
1387+
$message = isset($params['message']) ? $params['message'] : null;
1388+
1389+
if ($this->isBlank($message)) {
1390+
$message = $this->createMessage('timeRange', array('property' => $propertyName, 'min' => $min, 'max' => $max));
1391+
}
1392+
1393+
// Convert propertyValue to a time string if it's a DateTime object
1394+
if ($propertyValue instanceof DateTimeInterface) {
1395+
$valueTime = $propertyValue->format('H:i');
1396+
} else {
1397+
$valueTime = (string) $propertyValue;
1398+
}
1399+
1400+
// Normalize time strings to HH:MM format for consistent comparison
1401+
$valueTime = date('H:i', strtotime($valueTime));
1402+
$minTime = date('H:i', strtotime($min));
1403+
$maxTime = date('H:i', strtotime($max));
1404+
1405+
// Perform the time range validation
1406+
// We compare as strings because 'H:i' format allows direct string comparison for time
1407+
if ($valueTime < $minTime || $valueTime > $maxTime) {
1408+
throw new InvalidValueException($propertyName, $message);
1409+
}
1410+
}
1411+
}
13741412
}

0 commit comments

Comments
 (0)