Skip to content

Collection::save() doesn't clear the collection cached by CollectionStructure #15548

Closed

Description

@joshdaugherty

Bug description

Collection::save() doesn't clear the collection that CollectionStructure caches in Blink. After a collection is replaced by a new instance with different sites, its structure keeps answering from the old instance for the rest of the request, so structure()->in($site) returns null for a site the collection is now in.

CollectionStructure::collection() caches under this key:

return Blink::once('collection-structure-collection-'.$this->handle(), function () {
    return Collection::findByHandle($this->handle());
});

Collection::save() clears by prefix:

Blink::forget('collection-handles');
Blink::forget('mounted-collections');
Blink::flushStartingWith("collection-{$this->id()}");

collection-structure-collection-foo doesn't start with collection-foo, so the cached copy survives the save. existsIn() reads that copy, and in() only makes a tree when existsIn() is true, so it returns null. Callers chain straight onto it. For example, Entry::page() does $this->structure()->in($this->locale())->find($id) and fails with Call to a member function find() on null.

Collection::delete() has the same gap. It only clears mounted-collections, so the cached copy also survives a delete. Re-creating the collection goes through save(), so fixing save() also fixes delete-then-recreate.

How to reproduce

This test fails on 6.x (440eaf2):

#[Test]
public function structure_resolves_a_tree_after_the_collection_is_resaved_on_another_site()
{
    $this->setSites([
        'en' => ['url' => 'http://domain.com/'],
        'fr' => ['url' => 'http://domain.com/fr/'],
    ]);

    Collection::make('foo')->sites(['en'])->structureContents(['root' => false])->save();
    Collection::findByHandle('foo')->structure()->in('en'); // caches the sites:[en] instance

    Collection::make('foo')->sites(['fr'])->structureContents(['root' => false])->save();

    $this->assertNotNull(Collection::findByHandle('foo')->structure()->in('fr')); // null
}

It only fails when the saved object is a different instance from the cached one. If the same object is changed and saved again, the cached copy is that object and shows the new sites. In practice a different instance comes from anything that builds the collection fresh, such as an import or a test fixture.

Adding Blink::forget("collection-structure-collection-{$this->id()}"); to the cleanup block in save() makes the test pass. A fix can follow as a PR.

This is related to #5046, but it's a different key: that one is Tree's cached data, this one is the structure's cached collection.

Environment

Reproduced on Statamic 6.x at 440eaf2, in Statamic's own test suite with PreventSavingStacheItemsToDisk. v6.26.0 has the same code in both methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions