-
Notifications
You must be signed in to change notification settings - Fork 62
Fix filter compatibility's #236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Fix filter compatibility's #236
Conversation
|
It seems difficult to replicate Ruby's hash behavior in PHP. I will spend a little more time on it, but I think I will separate the PR for associative array handling. |
|
Thanks for the PR! LMK when you're ready for a review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR fixes compatibility issues with Ruby's Liquid implementation by updating several filters to match their Ruby counterparts. It addresses issues #233 and #234 by modifying the behavior of the capitalize filter, arithmetic filters (plus, minus, times, modulo, divided_by), and the map filter. The changes include adding utility functions isHash and isInteger to the Liquid class to support proper type detection.
Key Changes:
- Modified
capitalizeto only capitalize the first character and lowercase the rest (instead of capitalizing each word) - Updated arithmetic filters to return integers when both operands are integers, and floats otherwise
- Fixed
mapfilter to properly handle single hashes and nested arrays by adding hash detection and selective flattening
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| src/Liquid/StandardFilters.php | Updated capitalize, arithmetic filters (plus, minus, times, modulo, divided_by), and map filter for Ruby Liquid compatibility |
| src/Liquid/Liquid.php | Added utility methods isHash, isInteger, and modified arrayFlatten to support hash-aware flattening |
| tests/Liquid/LiquidTest.php | Added comprehensive tests for new isHash and isInteger utility methods, and arrayFlatten with skipHash parameter |
| tests/Liquid/StandardFiltersTest.php | Updated tests for capitalize behavior and added extensive test coverage for arithmetic filters with both integer and float operands |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { | ||
| return (float)$input / (float)$operand; | ||
| if (Liquid::isInteger($input) && Liquid::isInteger($operand)) { | ||
| return (int) floor($input / $operand); |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using floor() for integer division produces incorrect results for negative numbers. For example, floor(-14 / 3) returns -5 instead of the expected -4 (which PHP's integer division operator intdiv() or truncation would return). Consider using intdiv($input, $operand) for PHP 7.0+, or (int)($input / $operand) for consistent truncation behavior that matches most programming languages' integer division.
| return (int) floor($input / $operand); | |
| return intdiv($input, $operand); |
| $trimmed = ltrim($value, '-'); | ||
| return $trimmed !== '' && ctype_digit($trimmed); |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ltrim($value, '-') approach is unsafe for edge cases. For the string "-", ltrim removes the minus sign leaving an empty string, but the check $trimmed !== '' correctly returns false. However, consider strings like "--5" or "-+5" which would pass the empty check but fail ctype_digit(). While ctype_digit() will catch these, a cleaner approach would be to use a regex like /^-?\d+$/ which is more explicit and handles all cases in one check.
| $trimmed = ltrim($value, '-'); | |
| return $trimmed !== '' && ctype_digit($trimmed); | |
| return preg_match('/^-?\d+$/', $value) === 1; |
| if (empty($array)) { | ||
| return false; | ||
| } | ||
| return array_keys($array) !== range(0, count($array) - 1); |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Using !== for array comparison may be inefficient for large arrays. While this works correctly, array_keys($array) !== range(0, count($array) - 1) creates and compares two potentially large arrays. For PHP 8.1+, consider using the native !array_is_list($array) function with a version check fallback. Alternatively, iterate through keys and return early on the first non-sequential key for better performance.
| return array_keys($array) !== range(0, count($array) - 1); | |
| // Use native array_is_list() if available (PHP 8.1+) | |
| if (function_exists('array_is_list')) { | |
| return !array_is_list($array); | |
| } | |
| // Manual check for sequential integer keys starting from 0 | |
| $i = 0; | |
| foreach ($array as $key => $_) { | |
| if ($key !== $i++) { | |
| return true; | |
| } | |
| } | |
| return false; |
| public static function divided_by($input, $operand) | ||
| { | ||
| return (float)$input / (float)$operand; | ||
| if (Liquid::isInteger($input) && Liquid::isInteger($operand)) { | ||
| return (int) floor($input / $operand); | ||
| } | ||
| return (float) $input / (float) $operand; | ||
| } |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No division by zero handling in divided_by filter. When $operand is 0 (or evaluates to 0), this will result in a PHP warning/error and potentially INF or NAN. Consider adding a check and returning a sensible default (like 0 or null) or throwing a more descriptive exception, depending on how Ruby's Liquid handles this case.
| public static function modulo($input, $operand) | ||
| { | ||
| if (Liquid::isInteger($input) && Liquid::isInteger($operand)) { | ||
| return (int)$input % (int)$operand; | ||
| } | ||
| return fmod((float)$input, (float)$operand); | ||
| } |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No modulo by zero handling in modulo filter. When $operand is 0 (or evaluates to 0), this will result in a PHP warning "Division by zero" for the integer path (line 379) or undefined behavior for fmod(). Consider adding a check and handling this edge case appropriately, consistent with Ruby's Liquid behavior.
|
|
||
| // Flatten nested arrays while preserving hashes | ||
| // [[['attr' => 1]]] => [['attr' => 1]] | ||
| $input = Liquid::arrayFlatten($input, true); |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
map now flattens nested arrays via Liquid::arrayFlatten($input, true) before iterating, which increases the chance that elements evaluated below as callables (is_callable($elem)) will be surfaced and executed. An attacker controlling input can place nested PHP callables (e.g., "phpinfo", [ClassName, "method"], or a closure) so they are flattened and then invoked, enabling arbitrary function execution from templates. Remove callable invocation or strictly whitelist safe callables; e.g., delete the is_callable branch or change it to only allow vetted closures you create, and avoid flattening untrusted arrays before iteration:
// Either remove callable execution entirely
return array_map(function ($elem) use ($property) {
// no is_callable branch here
// ... only property extraction logic
}, $input);
// Or, if callable support is strictly needed, restrict:
if ($elem instanceof \Closure && $this->isTrustedClosure($elem)) {
return $elem();
}|
Will it help if I'd merge #237? |
|
@sanmai That would be greatly appreciated, thank you! |
|
Done! Feel free to merge with the main branch. |
|
My apologies for the silence. I'll get a PR ready this weekend, organizing what can be merged first. |
vendor/bin/phpunitbuild/coverage/index.htmlvendor/bin/php-cs-fixer fixFix #233 , #234
Changes
capitalizeplus, minus, times, modulo, divided_bymapWhat I'd like reviewed
src/Liquid/Liquid.php::isHash,isIntegerI'm currently adding utility functions directly to Liquid/Liquid.
I did consider making a separate Liquid/Util module, but I generally avoid creating modules with such broad, generic purposes.
Given the small scope right now, I think keeping them in Liquid/Liquid is fine for the time being.
However, if this section grows significantly, it will definitely require more maintenance. So, I'd appreciate your decision on how to proceed.