Skip to content

Commit 50a0f1c

Browse files
authored
Merge pull request #40 from keepsuit/shopify-5.7
Shopify liquid 5.7 changes
2 parents 972ed8c + 722da59 commit 50a0f1c

File tree

3 files changed

+321
-24
lines changed

3 files changed

+321
-24
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Liquid is a template engine with interesting advantages:
1818

1919
| PHP Liquid | Shopify Liquid |
2020
|------------:|---------------:|
21+
| v0.8 | v5.7 |
2122
| v0.7 | v5.6 |
2223
| v0.1 - v0.6 | v5.5 |
2324

@@ -40,11 +41,11 @@ Create a new environment factory instance:
4041

4142
```php
4243
$environment = \Keepsuit\Liquid\EnvironmentFactory::new()
43-
// enable strict variables mode
44+
// enable strict variables mode (disabled by default)
4445
->setStrictVariables(true)
45-
// enable strict filters mode
46+
// enable strict filters mode (disabled by default)
4647
->setStrictFilters(true)
47-
// rethrow exceptions instead of rendering them
48+
// rethrow exceptions instead of rendering them (disabled by default)
4849
->setRethrowErrors(true)
4950
// disable lazy parsing (enabled by default)
5051
->setLazyParsing(false)

src/Filters/StandardFilters.php

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -675,34 +675,69 @@ public function urlEncode(string|int|float|null $input): string
675675
/**
676676
* Filters an array to include only items with a specific property value.
677677
*/
678-
public function where(iterable $input, ?string $property = null, mixed $targetValue = null): array
678+
public function where(iterable $input, string $property, mixed $targetValue = null): array
679679
{
680-
$input = $this->mapToLiquid($input);
680+
$input = $this->iterableToList($input);
681681

682-
if ($input === []) {
683-
return [];
682+
return array_values(array_filter($input, fn (mixed $item) => $this->objectHasPropertyWithValue($item, $property, $targetValue)));
683+
}
684+
685+
/**
686+
* Filters an array to exclude items with a specific property value.
687+
*/
688+
public function reject(iterable $input, string $property, mixed $targetValue = null): array
689+
{
690+
$input = $this->iterableToList($input);
691+
692+
return array_values(array_filter($input, fn (mixed $item) => ! $this->objectHasPropertyWithValue($item, $property, $targetValue)));
693+
}
694+
695+
/**
696+
* Tests if any item in an array has a specific property value.
697+
*/
698+
public function has(iterable $input, string $property, mixed $targetValue = null): bool
699+
{
700+
$input = $this->iterableToList($input);
701+
702+
foreach ($input as $item) {
703+
if ($this->objectHasPropertyWithValue($item, $property, $targetValue)) {
704+
return true;
705+
}
684706
}
685707

686-
$input = array_is_list($input) ? $input : [$input];
708+
return false;
709+
}
710+
711+
/**
712+
* Returns the first item in an array with a specific property value.
713+
*/
714+
public function find(iterable $input, string $property, mixed $targetValue = null): mixed
715+
{
716+
$input = $this->iterableToList($input);
687717

688-
$result = array_filter($input, function (mixed $value) use ($property, $targetValue) {
689-
if ($targetValue === null) {
690-
return match (true) {
691-
is_string($value) && $property !== null => str_starts_with($value, $property),
692-
is_array($value) && $property !== null => (bool) ($value[$property] ?? null),
693-
is_object($value) && $property !== null => (bool) ($value->$property ?? null),
694-
default => (bool) $value,
695-
};
718+
foreach ($input as $item) {
719+
if ($this->objectHasPropertyWithValue($item, $property, $targetValue)) {
720+
return $item;
696721
}
722+
}
697723

698-
return match (true) {
699-
is_array($value) && $property !== null => ($value[$property] ?? null) === $targetValue,
700-
is_object($value) && $property !== null => ($value->$property ?? null) === $targetValue,
701-
default => false,
702-
};
703-
});
724+
return null;
725+
}
704726

705-
return array_values($result);
727+
/**
728+
* Returns the index of the first item in an array with a specific property value.
729+
*/
730+
public function findIndex(iterable $input, string $property, mixed $targetValue = null): ?int
731+
{
732+
$input = $this->iterableToList($input);
733+
734+
foreach ($input as $index => $item) {
735+
if ($this->objectHasPropertyWithValue($item, $property, $targetValue)) {
736+
return $index;
737+
}
738+
}
739+
740+
return null;
706741
}
707742

708743
protected function mapToLiquid(iterable $input): array
@@ -717,4 +752,38 @@ protected function mapToLiquid(iterable $input): array
717752
return $value;
718753
});
719754
}
755+
756+
/**
757+
* Convert an iterable to a list.
758+
*/
759+
protected function iterableToList(iterable $input): array
760+
{
761+
$input = $this->mapToLiquid($input);
762+
763+
if ($input === []) {
764+
return [];
765+
}
766+
767+
return array_is_list($input) ? $input : [$input];
768+
}
769+
770+
/**
771+
* Check if an object has a property with a specific value.
772+
* If item is a string, check if it starts with the property name.
773+
*/
774+
protected function objectHasPropertyWithValue(mixed $item, string $property, mixed $targetValue = null): bool
775+
{
776+
$value = match (true) {
777+
is_array($item) => ($item[$property] ?? null),
778+
is_object($item) => ($item->$property ?? null),
779+
$targetValue === null && is_string($item) && str_starts_with($item, $property) => true,
780+
default => null,
781+
};
782+
783+
if ($targetValue === null) {
784+
return $value !== false && $value !== null && $value !== '' && $value !== [];
785+
} else {
786+
return $value === $targetValue;
787+
}
788+
}
720789
}

tests/Integration/StandardFilterTest.php

Lines changed: 228 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,9 +652,35 @@
652652
['handle' => 'alpha', 'ok' => true],
653653
['handle' => 'delta', 'ok' => true],
654654
];
655-
656655
expect($this->filters->invoke($this->context, 'where', $input, ['ok', true]))->toBe($expectation);
657656
expect($this->filters->invoke($this->context, 'where', $input, ['ok']))->toBe($expectation);
657+
658+
$template = "{{ array | where: 'ok' | map: 'handle' | join: ' ' }}";
659+
assertTemplateResult('alpha delta', $template, ['array' => $input]);
660+
});
661+
662+
test('where with value', function () {
663+
$input = [
664+
['handle' => 'alpha', 'ok' => true],
665+
['handle' => 'beta', 'ok' => false],
666+
['handle' => 'gamma', 'ok' => false],
667+
['handle' => 'delta', 'ok' => true],
668+
];
669+
670+
$template = "{{ array | where: 'ok', true | map: 'handle' | join: ' ' }}";
671+
assertTemplateResult('alpha delta', $template, ['array' => $input]);
672+
});
673+
674+
test('where with false value', function () {
675+
$input = [
676+
['handle' => 'alpha', 'ok' => true],
677+
['handle' => 'beta', 'ok' => false],
678+
['handle' => 'gamma', 'ok' => false],
679+
['handle' => 'delta', 'ok' => true],
680+
];
681+
682+
$template = "{{ array | where: 'ok', false | map: 'handle' | join: ' ' }}";
683+
assertTemplateResult('beta gamma', $template, ['array' => $input]);
658684
});
659685

660686
test('where string keys', function () {
@@ -774,3 +800,204 @@
774800

775801
expect($t->value)->toBe(1);
776802
});
803+
804+
test('find with value', function () {
805+
$products = [
806+
['title' => 'Pro goggles', 'price' => 1299],
807+
['title' => 'Thermal gloves', 'price' => 1499],
808+
['title' => 'Alpine jacket', 'price' => 3999],
809+
['title' => 'Mountain boots', 'price' => 3899],
810+
['title' => 'Safety helmet', 'price' => 1999],
811+
];
812+
813+
$template = <<<'LIQUID'
814+
{%- assign product = products | find: 'price', 3999 -%}
815+
{{- product.title -}}
816+
LIQUID;
817+
818+
assertTemplateResult('Alpine jacket', $template, ['products' => $products]);
819+
});
820+
821+
test('find on empty array', function () {
822+
expect($this->filters->invoke($this->context, 'find', [], ['foo', 'bar']))->toBeNull();
823+
824+
$template = <<<'LIQUID'
825+
{%- assign product = products | find: 'title.content', 'Not found' -%}
826+
{%- unless product -%}
827+
Product not found.
828+
{%- endunless -%}
829+
LIQUID;
830+
831+
assertTemplateResult('Product not found.', $template, ['products' => []]);
832+
});
833+
834+
test('find index with value', function () {
835+
$products = [
836+
['title' => 'Pro goggles', 'price' => 1299],
837+
['title' => 'Thermal gloves', 'price' => 1499],
838+
['title' => 'Alpine jacket', 'price' => 3999],
839+
['title' => 'Mountain boots', 'price' => 3899],
840+
['title' => 'Safety helmet', 'price' => 1999],
841+
];
842+
843+
$template = <<<'LIQUID'
844+
{%- assign index = products | find_index: 'price', 3999 -%}
845+
{{- index -}}
846+
LIQUID;
847+
848+
assertTemplateResult('2', $template, ['products' => $products]);
849+
});
850+
851+
test('find index on empty array', function () {
852+
expect($this->filters->invoke($this->context, 'find_index', [], ['foo', 'bar']))->toBeNull();
853+
854+
$template = <<<'LIQUID'
855+
{%- assign index = products | find_index: 'title.content', 'Not found' -%}
856+
{%- unless index -%}
857+
Index not found.
858+
{%- endunless -%}
859+
LIQUID;
860+
861+
assertTemplateResult('Index not found.', $template, ['products' => []]);
862+
});
863+
864+
test('has', function () {
865+
$input = [
866+
['handle' => 'alpha', 'ok' => true],
867+
['handle' => 'beta', 'ok' => false],
868+
['handle' => 'gamma', 'ok' => false],
869+
['handle' => 'delta', 'ok' => true],
870+
];
871+
872+
assertTemplateResult(
873+
'true',
874+
'{{ array | has: "ok" }}',
875+
['array' => $input],
876+
);
877+
878+
assertTemplateResult(
879+
'true',
880+
'{{ array | has: "ok", true }}',
881+
['array' => $input],
882+
);
883+
});
884+
885+
test('has when does not have it', function () {
886+
$input = [
887+
['handle' => 'alpha', 'ok' => false],
888+
['handle' => 'beta', 'ok' => false],
889+
['handle' => 'gamma', 'ok' => false],
890+
['handle' => 'delta', 'ok' => false],
891+
];
892+
893+
assertTemplateResult(
894+
'false',
895+
'{{ array | has: "ok" }}',
896+
['array' => $input],
897+
);
898+
899+
assertTemplateResult(
900+
'false',
901+
'{{ array | has: "ok", true }}',
902+
['array' => $input],
903+
);
904+
});
905+
906+
test('has on empty array', function () {
907+
expect($this->filters->invoke($this->context, 'has', [], ['foo', 'bar']))->toBe(false);
908+
909+
$template = <<<'LIQUID'
910+
{%- assign has_product = products | has: 'title.content', 'Not found' -%}
911+
{%- unless has_product -%}
912+
Product not found.
913+
{%- endunless -%}
914+
LIQUID;
915+
assertTemplateResult('Product not found.', $template, ['products' => []]);
916+
});
917+
918+
test('has with false value', function () {
919+
$input = [
920+
['handle' => 'alpha', 'ok' => true],
921+
['handle' => 'beta', 'ok' => false],
922+
['handle' => 'gamma', 'ok' => false],
923+
['handle' => 'delta', 'ok' => true],
924+
];
925+
926+
assertTemplateResult(
927+
'true',
928+
'{{ array | has: "ok", false }}',
929+
['array' => $input],
930+
);
931+
});
932+
933+
test('has with false value when does not have it', function () {
934+
$input = [
935+
['handle' => 'alpha', 'ok' => true],
936+
['handle' => 'beta', 'ok' => true],
937+
['handle' => 'gamma', 'ok' => true],
938+
['handle' => 'delta', 'ok' => true],
939+
];
940+
941+
assertTemplateResult(
942+
'false',
943+
'{{ array | has: "ok", false }}',
944+
['array' => $input],
945+
);
946+
});
947+
948+
test('join calls to liquid on each element', function () {
949+
$drop = new class implements \Keepsuit\Liquid\Contracts\MapsToLiquid
950+
{
951+
public function toLiquid(): string
952+
{
953+
return 'i did it';
954+
}
955+
};
956+
957+
expect($this->filters->invoke($this->context, 'join', [$drop, $drop], [', ']))->toBe('i did it, i did it');
958+
});
959+
960+
test('reject', function () {
961+
$input = [
962+
['handle' => 'alpha', 'ok' => true],
963+
['handle' => 'beta', 'ok' => false],
964+
['handle' => 'gamma', 'ok' => false],
965+
['handle' => 'delta', 'ok' => true],
966+
];
967+
968+
assertTemplateResult(
969+
'beta gamma',
970+
'{{ array | reject: "ok" | map: "handle" | join: " " }}',
971+
['array' => $input],
972+
);
973+
});
974+
975+
test('reject with value', function () {
976+
$input = [
977+
['handle' => 'alpha', 'ok' => true],
978+
['handle' => 'beta', 'ok' => false],
979+
['handle' => 'gamma', 'ok' => false],
980+
['handle' => 'delta', 'ok' => true],
981+
];
982+
983+
assertTemplateResult(
984+
'beta gamma',
985+
'{{ array | reject: "ok", true | map: "handle" | join: " " }}',
986+
['array' => $input],
987+
);
988+
});
989+
990+
test('reject with false value', function () {
991+
$input = [
992+
['handle' => 'alpha', 'ok' => true],
993+
['handle' => 'beta', 'ok' => false],
994+
['handle' => 'gamma', 'ok' => false],
995+
['handle' => 'delta', 'ok' => true],
996+
];
997+
998+
assertTemplateResult(
999+
'alpha delta',
1000+
'{{ array | reject: "ok", false | map: "handle" | join: " " }}',
1001+
['array' => $input],
1002+
);
1003+
});

0 commit comments

Comments
 (0)