-
Notifications
You must be signed in to change notification settings - Fork 822
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
ENH Looping through arrays in templates #11244
ENH Looping through arrays in templates #11244
Conversation
73b43db
to
9699a71
Compare
* @return Object|DBField | ||
* @return object|DBField |
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.
It's probably the case that this used to mean the old SS3 Object
which later became SS_Object
which is effectively ViewableData
now from a typing perspective.
That said, the way this is implemented, it really can return any object, so I'm going to treat this as though it was just a capital letter away from being correct initially.
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.
Update now that this PR uses ArrayList
instead of ArrayIterator
:
There are still other scenarios where this method could return arbitrary objects, because of the way it's been implemented. It might make more sense to tighten this up to require ViewableData
to be returned but for now that's just not how it works - and doing that should be handled in a separate PR.
f7b853e
to
154eb52
Compare
154eb52
to
0a90f5d
Compare
ea0d937
to
3298747
Compare
if (is_object($value) && !$value instanceof ViewableData) { | ||
if (is_object($value) && !($value instanceof ViewableData) && !is_iterable($value)) { | ||
return new ArrayData($value); |
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.
This is needed because otherwise ArrayIterator
and other iterators get shoved inside ArrayData
which is obviously not correct and results in not being able to get the count.
Arguably this is a bug fix
3298747
to
6ce976a
Compare
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.
Can you help me understand what is meant by `Not wrapping in ArrayList because that currently throws exceptions if indexed arrays are passed in, and if we changed that it would silently fail if you try to call Filter() or Sort() or Find() etc.
Just doing a quick test on CMS 5 post merging the scalars PR
I can pass this in from a controller
public function ArrayListTest()
{
// note the decending order
return new ArrayList(['item3', 'item2', 'item1']);
}
And render it in a template
<!-- works - item3, item2, item1 -->
<% loop $ArrayListTest %>
$Me
<% end_loop %>
<!-- works -->
$ArrayListTest.Count
<!-- though this will never work instead it will "silently fail" - item3, item2, item1
Sort requires a column -->
<% loop $ArrayListTest.Sort %>
$Me
<% end_loop %>
Seem like the major reason for ArrayIterator is because methods like .Sort
can't work, because we don't have ArrayData() or another object as the members in the ArrayList, and you'd like a hard error thrown rather than nothing happening to protected against developer error?
Though that seems like a strange reason to not use ArrayList? Because you can already construct an ArrayList full of scalars and use it in a template. It's just called Sort() on it with no parameters does nothing ... though that's what is supposed to happen?
I'm not sure what's so wrong with ArrayList's at we'd want to complicate things by using ArrayIterator() instead which increases the number of scenarios that need to be coded for / things that could go wrong.
I feel like I'm missing something important here?
First of all I didn't say anything about no parameters - whether you pass parameters or not it'll have undefined behaviour and most likely fail silently. The assumption is that you can sort and filter arraylists. Remember this affects PHP-land logic as well, not just template logic. If we want to wrap indexed arrays in We then need to decide what happens if someone calls We also have to verify what will happen if you try to feed that into a gridfield, or anywhere else that currently expects Basically trying to use |
It does seem a little odd to be concerned about all these things - as mentioned before in CMS 5 you can currently go Based on what you're suggesting here seems like we may even want to actually restrict the constructor of ArrayList to disallow a index array scalars entirely since you can't use most of the ArrayList API on them? I am actually open to that idea as it would restrict the number of possible codepaths and scenarios that need to be considered ... though obviously it negates the whole idea of just return and array of scalars in a controller for template consumption. Maybe this card is just a bad idea? I'm starting to now wonder what ArrayList methods with a list of values can and can't be used in a template. Count can, though sort and friends can't, though are there other that can? Should be making it so that arrays can have the exact same methods called on to match ArrayList? Feels like by allowing mixed types we're just making things worse... typically we want to go in the direction of being more restrictive to make things more robust e.g. strong typing, this feels like it's going in the wrong direction |
It's not so much about gridfield (that was just one example) as it is about the API for
That would be great, as a separate issue. This issue isn't about deciding how to fix the problems with arraylist.
No it doesn't, as this PR avoids using
I think this card is a great idea, and I think we can worry about
With this PR, |
Do we need to have a meeting to discuss this one or can we get it merged? |
My main concern here is that by not using ArrayList + ArrayData we're increasing the complexity of the number of scenarios that need to be managed. As an alternate solution, would it work if arrays were converted right before going into the template to a combination of ArrayList and ArrayData with a single magical "_index" key used in the ArrayData if an index array was passed into ArrayList? e.g. ['item1', 'item2']; Automatically gets converted to new ArrayList([
new ArrayData['_index' => 'item1'],
new ArrayData['_index' => 'item2'],
]); And then have some logic where if the only key in the ArrayData is Seems like this would negate the need for all the extra logic to handle iterables / countables etc? |
It's not only templates though, it's also PHP. So if you do that, you're also saying any time you pass a non-associative array to If you're really insistent on this being handled using Looking at this PR again, there's not that much complexity being added to account for non-
Choosing to wrap this in
I think we'd quickly find trying to deal with |
So again.
|
Probably have a chat to discuss I've quickly together a PR to demo the idea of just using an ArrayList #11256, seems like it does the same thing with much less code changes required? (I've probably missed something though) |
We've discussed offline - I had a faulty assumption about |
if (is_array($item)) { | ||
if (is_array($item) && !array_is_list($item)) { |
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.
Ultimately the way ArrayList
works needs to be re-evaluated at some stage anyway - only converting in the iterator but not when fetching items directly via find()
and other methods leads to unexpected results. So I think this is fine for now even though it allows a setup that used to cause an explicit error.
if (is_array($this->item)) { | ||
$this->itemIterator = new ArrayIterator($this->item); | ||
} elseif ($this->item instanceof Iterator) { | ||
$this->itemIterator = $this->item; | ||
} else { |
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.
I think it's worth adding this because:
- there's already logic here to handle native arrays, even though previously those couldn't be used at all and now they're wrapped in
ArrayList
before we get here... but this allows for them to be handled correctly if they somehow sneak in - As with the array check, this allows iterators to be used correctly if they somehow sneak in. They shouldn't get here, but if they do it's better to just let them be iterated over rather than throw exceptions about it, IMO.
$result = $this->obj($field, $arguments, $cache); | ||
return $result->exists(); | ||
if ($result instanceof ViewableData) { | ||
return $result->exists(); | ||
} | ||
return (bool) $result; |
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.
This is still needed because as mentioned above the obj()
method can return arbitrary objects with its current implementation. It's not related to the issue at hand but worth keeping IMO.
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.
Some of the added tests are no longer explicitly required for the new functionality - but they weren't being tested before so we're adding additional (if only tangentially related to the current issue) test coverage.
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.
Code changes look good, though this one should be run though a kitchen-sink CI run with the PR included since it's a pretty low level change
One thing I did notice is that while <% loop $Me %>
worked as expected <% loop %>
did not, possibly the PR just needs a rebase or merge-up though
Test code:
PageController
public function OneList()
{
return ['abc', 'def'];
}
public function OneAssoc()
{
return ['Foo' => 'bar'];
}
public function TwoList()
{
return [
['abc', 'def'],
['ghi', 'xyz'],
];
}
public function TwoAssoc()
{
return [
[
'Food' => 'bars',
'Bazz' => 'buzz',
],
];
}
public function ThreeList()
{
return [
[
['123', '456'],
],
[
['789', '000'],
]
];
}
public function ThreeAssoc()
{
return [
[
[
'Foody' => 'barsy',
'Bazzy' => 'buzzy',
],
],
];
}
Page.ss
<% loop $OneList %>
$Me<br>
<% end_loop %>
$OneAssoc.Foo<br>
<% loop $TwoList %>
<% loop $Me %>
$Me<br>
<% end_loop %>
<% end_loop %>
<% loop $TwoAssoc %>
$Me.Food<br>
$Me.Bazz<br>
<% end_loop %>
<% loop $ThreeList %>
<% loop $Me %>
<% loop $Me %>
$Me<br>
<% end_loop %>
<% end_loop %>
<% end_loop %>
<% loop $ThreeAssoc %>
<% loop $Me %>
$Me.Foody<br>
$Me.Bazzy<br>
<% end_loop %>
<% end_loop %>
This PR started before that work got done, so yeah that'd need to be merged up to |
Had to rebase in the end to get kitchen sink CI to run happily. That CI run is linked in the issue. |
edc33c4
to
f9e757a
Compare
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.
Regrettably, the Me
deprecation in ViewableData
caused other problems in sink, because against all reason it's being used in PHP contexts.
I've removed that from this PR and have opened a new issue for it.
That has also stopped the ArrayIterator
tests in SSViewerTest
from working (because there's no Me()
method on that class) so I've removed those tests as well.
f9e757a
to
29793a3
Compare
29793a3
to
d8c8478
Compare
d8c8478
to
66466c8
Compare
Wrapping arrays in I've opened a separate card to look into all that but for now I've removed the logic wrapping arrays in |
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.
Tested locally, works well
Description
Allows arrays to be passed into templates and iterated over.
Associative arrays are wrapped in
ArrayData
so their values can be accessed by key.Indexed arrays are wrapped in
ArrayIterator
ArrrayList
when fetched fromViewableData::obj()
to respect the method signature.Not wrapping inArrayList
because that currently throws exceptions if indexed arrays are passed in, and if we changed that it would silently fail if you try to callFilter()
orSort()
orFind()
etc.Issues