Skip to content
● Grav 2.2 is out: 2x faster cold starts and 96% less memory on big sites. Read the announcement →

How to clone a flex object in twig

twig

Solved by Vadym View solution

Started by complanar 6 days ago · 6 replies · 262 views
6 days ago

I have a simple list of events implemented as flex objects. Some of them are recurring events, so the have to be displayed multiple times at the page, even as the only exist once in the storage. How can I achive this?

What I have tried so far is:

  • get all recurring events ordered by date with {% set recurringEvents = collection.filterBy({ recurring: true}).sort({begindate: 'ASC'}).copy() %}
  • loop over all events %{ for event in recurringEvents %} and adjust their begindate by adding for example one month.
  • update their property {% do event.setProperty('begindate', newbegindate %}
  • add them back to the original collection with {% do collection.add(event) %}

This works as long as a recurring event does not need to be in the time span (e.g. one year) multiple times. If it would occur multiple times (e.g. every month) the event can be added multiple times to the collection, but only by reference, so all events then all share the properties of the last processed event.

Calling event.clone() as suggested in the documentation does not work, it simply does nothing and event.createCopy() throws an error in twig:

"Cannot create new object (Already exists)"

Maybe I am understanding the documentation wrong. Or does .clone() only work in PHP? I notice that in the documentation there is no flex object method .clone() listed …

I can clone events by copying the whole collection, but this is not what I want to do. Do I have to create a collection of a single object and then clone this one? There must be another way, I guess.

Can someone point me in the right direction?

last edited 09/29/26 by complanar
5 days ago

I just tried https://github.com/paulcmal/grav-plugin-twig-clone . This way the flex object can be cloned, but nevertheless the instances share their properties (maybe because they still have the same key .getKey(). I did not find a way to change the key of the cloned object before adding it to the collection.

I managed to generate a new key and createCopy(newKey) a new object. That addresses the above mentioned error ("Cannot create new object") but saves the cloned flex object to storage. I do not want that as this would clutter the disk with unconnected clones of the same event. I could delete them directly afterwards, but this would happen at every page request (or maybe at cache update?) and create a lot of write-to-disk overhead.

5 days ago

@complanar, take a look at the Eventlist plugin by @bitstarr, it uses Flex Objects and seems to contain the logic you need.

You can also use the toArray() method to get an array copy and manipulate it inside the loop using standard Twig filters, assuming I’ve understood your goal correctly.

5 days ago

Thank you very much. I will have a look into it. From what I can tell by a quick look at the code at the Eventlist plugin by @bitstarr , recurring events are displayed in a separate list and not multiple times in a linear timeline. This is not what I wanted and would not solve my problem.

Your second approach does not solve my problem either. I need a deep-copy of a flex object and not a reference. The array still contains references of the flex objects and not copies, as far I can tell, though I did not fully understand the flex object source code. This way manipulating one "instance" of an event will change all "instances" of the recurring event.

I am sorry, that I could not make my point clear yet, give me the opportunity to explain it again.

  • I have a flex directory with events, which in the simplest form would only contain a title and a date

    YAML
    title: 'Simple Event'
    date: '2026-09-30'
    
  • I have one single flex object which represents a recurring event, lets say it repeats every month on the 3rd day, starting at September 3rd. Its simplified flex blueprint structure would be:

    YAML
    title: 'Recurring Event'
    recurring: true
    interval: monthly
    date: 2026-09-03
    
  • I a linear timeline the latter would be printed every month (Sep 3rd, Oct 3rd, Nov 3rd, …) while the other one only once in the middle of the list at Sep 30th.

  • To have one flex object be displayed multiple times (in correct order!) I would have to clone the flex object in memory (not at disk) multiple times, modify its date (set it to 2026-10-03, 2026-11-03 and so on). Only so I am able to sort the events correct by their date.

  • If I simply use a new variable for the event in twig {% set newEvent = event %} I only get a reference of the flex object, not a copy. That's my whole problem. From what I understand in the documentation collection.copy() should return copies of the object, not references, but I did not get that right in my code.

  • object.createCopy() creates a copy at disk, not in memory.

  • Ridiculous as it sounds, I tried to get a deep copy of an event by creating an (empty) collection, adding one object and copying the whole collection. As there is no way to instantiate a collection from scratch, I created a copy of my whole collection and cleared it with {% set emptyCollection = eventCollection.copy().clear() %}. Afterwards I added the recurring event and tried to get it back with {% set recurringEventCopy = eventCollection.add(recurringEvent).copy().first() %}. Somehow – I do not know why – recurringEventCopy is still connected with recurringEvent. Changing the date of the copy by {% recurringEventCopy.setProperty('date', '2026-10-03') %} affects the date property of recurringEvent. Chaining that multiple times – as one copy is not enough – unsurprisingly reproduces the problem. All of the objects are connected so at the end I have a multitude of recurringEvent copies that all share the same date (the last one I set).

Maybe @rhuk has an idea what's wrong with my approach or if there really is no possibility to achieve what I want at the moment.

4 days ago Solution

Here is a working solution that builds the recurring timeline array directly within Twig. I added the until property (the end date for repetitions) and published (status flag) to the object processing logic.

TWIG
{# 1. Query published events once #}
{% set raw_events = grav.get('flex').collection('events').filterBy({ published: true }).toArray() %}
{% set timeline = [] %}
{% for id, item in raw_events %}
    {% set is_recurring = item.recurring ?? false %} 
    {% set base_event = {
        'id': id,
        'title': item.title,
        'date': item.date,
        'recurring': is_recurring,
        'interval': item.interval ?? null,
        'until': item.until ?? null,
        'is_instance': false
    } %}
    {# Add the original base event #}
    {% set timeline = timeline|merge([base_event]) %}
    {# Generate recurring instances #}
    {% if is_recurring and base_event.until %}
        {% set until_timestamp = (base_event.until ~ ' 23:59:59')|date('U') %}
        {% set modifier = {
            'weekly': '+1 week',
            'monthly': '+1 month',
            'yearly': '+1 year'
        }[base_event.interval] ?? '+1 month' %}
        {% set start_year = base_event.date|date('Y') %}
        {% set until_year = base_event.until|date('Y') %}
        {% set start_month = base_event.date|date('n') %}
        {% set until_month = base_event.until|date('n') %}
        {# Calculate total calendar months between start and until dates #}
        {% set total_months = (until_year - start_year) * 12 + (until_month - start_month) + 1 %}
        {# Dynamic loop limit based on interval #}
        {% switch base_event.interval %}
          {% case 'weekly' %}
            {% set limit = (total_months * 4.5)|round(0, 'ceil') %}
          {% case 'monthly' %}
            {% set limit = total_months %}
          {% default %}
            {% set limit = until_year - start_year + 1 %}
        {% endswitch %}
        {% set current_date = base_event.date %}
        {% if limit >= 1 %}
            {% for i in 1..limit %}
                {% set current_date = current_date|date_modify(modifier)|date('Y-m-d H:i:s') %}
                {% if current_date|date('U') <= until_timestamp %}
                    {% set instance_event = base_event|merge({
                        'date': current_date,
                        'is_instance': true
                    }) %}
                    {% set timeline = timeline|merge([instance_event]) %}
                {% endif %}
            {% endfor %}
        {% endif %}
    {% endif %}
{% endfor %}
{# 2. Sort the final combined timeline chronologically #}
{% set sorted_timeline = timeline|sort_by_key('date') %}
{# 3. Render output #}
<div id="list">
    {% for item in sorted_timeline %}
        <div style="margin-bottom: 2rem;">
            <h4>{{ item.title }}</h4>
            <p>{{ item.date|date("d.m.Y") }}</p>
        </div>
    {% endfor %}
</div>

However, considering your scenario, it might actually be better practice to automatically compute and attach a recurring_dates array to the Flex Object on the PHP side during creation/saving using the setProperty() method. This way, whenever the until parameter changes, the dates update in the background rather than being calculated dynamically on every page render.

For example, in one of my custom plugins, I append a year property to objects upon creation so I can easily filter entries by year later. Since there's no corresponding field in the blueprint, it stays completely hidden in the Admin Panel.

Here is an example method from @bitstarr plugin that I used as a starting point for my own implementation.

With a little help from AI and working examples of plugins utilizing Flex Objects, you should be able to get a backend PHP solution up and running in no time.

Keep us posted, and good luck with the project!

4 days ago

Thank you very much for that detailed reply. I will have a look into it (could take a few days, as I am busy with some other things) and see, if it solves the problem.

I already thought of creating "virtual objects" of some kind to represent the recurring dates, but as i am new to grav, I was reluctant to create a plugin for that with which I have no experience. At the moment it is simply some blueprints and templates inside of a customized copy of the quark2 theme.

1 day ago

Workes very well now, thank you again. Especially the |date_modify filter was an eye opener – somehow I missed this one until now.

I'am still looking for a better solution, maybe I will try your suggestion by creating a plugin later. For the moment you helped me to get the page running. Your code was an almost perfect fit.

In the post above simplified my example for demonstrating purposes and what still troubles me, is the repetition of code (because in reality the events are composed of much more properties (location, different possibilities for their duration, additional media, descriptions, links to other pages etc.) All this has to be kind of replicated within twig, which makes the code complicated and slows everything down.

Suggested topics

Topic Participants Replies Views Activity
Plugins · by BenLaKnet, 3 days ago
1 93 3 days ago
Plugins · by complanar, 4 weeks ago
2 414 3 weeks ago
Plugins · by Vladimir Novak, 2 months ago
3 705 2 months ago
Plugins · by Mathieu Lecouturier, 2 months ago
3 544 2 months ago
Plugins · by Slebeig, 3 months ago
1 658 3 months ago