Skip to content
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

[4.x] Hooks #9481

Merged
merged 14 commits into from Feb 23, 2024
Merged

[4.x] Hooks #9481

merged 14 commits into from Feb 23, 2024

Conversation

ryanmitchell
Copy link
Contributor

@ryanmitchell ryanmitchell commented Feb 8, 2024

Edit by Jason:
This PR has changed substantially since this initial description. Check the comments below for more details.


This PR adds the concept of 'hooks' into points in the tag lifecycle, allowing for modification of the state of the tag at that point.

Combined with macroable, this allows for reuse of the base tags in some circumstances where otherwise a custom tag would be required.

Usage is as follows:

\Statamic\Tags\Collection::addSetupHook(fn ($tag) => $tag->setParameters(['new_params' => 1));

\Statamic\Tags\Collection::addSetupHook([\App\MyClass::class, 'myMethod']);

Ideally this PR would also allow for a hook into the output of the tag (eg a render or processed hook) but I couldn't see how to do that without affecting the magic __call() on all tags that implement it. Maybe you have some bright ideas on an approach for that? Failing that, we could add hook into the output() method.

@jasonvarga
Copy link
Member

We thought through how this might end up being used and ended up on using Laravel's Pipeline feature.

tl;dr It lets you pass something through multiple closures. Each closure does something and passes onto the $next closure. It's basically how middleware work.

We figured that this feature would eventually be used to modify stuff - for example the entries that get returned from a collection tag. So, we needed a way to be able to pass something through and finally return it.

use Statamic\Tags\Collection as CollectionTag;

CollectionTag::addHook('fetched-entries', function ($entries, $next) {
  // Take the first 5 items and convert them into custom entry instances.
  $entries = $entries->take(5)->mapInto(CustomEntry::class);

  // Then pass it along to any other registered hook closures, and finally 
  // back to the tag where the hook was triggered.
  return $next($entries);
});

Then there's also the use case that I know you need your project, which is to perform some logic on the final version of a thing. For example, grabbing all the entry IDs that would be output in a listing.

CollectionTag::addHook('fetched-entries', function ($entries, $next) {
  // By calling $next() first, if there are other hooks that *do* modify entries, you'll be able 
  // to get the end result even if your hook is registered before theirs.
  $entries = $next($entries);

  $ids = $entries->pluck('id');

  return $entries;
});

Also, inside the closure, $this is the tag class itself so you can write code as if you're actually in that class, just like a macro. Here's a silly example where maybe you want to reuse a bunch of parameters everywhere.

{{ collection preset="basics" }} ... {{ /collection }}
CollectionTag::addHook('init', function ($value, $next) {
  if ($this->params->get('preset') === 'basics') {
      $this->params->merge([
          'from' => 'articles',
          'limit' => '5',
          'as' => 'articles',
      ]);
  }

  return $next($value);
});

Aside from the functionality itself, this PR adds the init hook to all tags, plus fetching-entries and fetched-entries to the appropriate places within the collection tag.

@jasonvarga jasonvarga changed the title [4.x] Add 'setup hooks' to tags to allow modification of tag state [4.x] Tag hooks Feb 22, 2024
@ryanmitchell
Copy link
Contributor Author

This looks great. Using a pipeline is a great idea.

Would it also be worth adding an ‘output’ hook in the output method? That way most tags would be covered by default?

@jasonvarga
Copy link
Member

We could, but I don't like adding code "just because". If someone shows a use case for it, sure, we can add it.

@jasonvarga jasonvarga mentioned this pull request Feb 23, 2024
@jasonvarga jasonvarga changed the title [4.x] Tag hooks [4.x] Hooks Feb 23, 2024
@jasonvarga
Copy link
Member

jasonvarga commented Feb 23, 2024

Some more tweaks:

  • This feature isn't just for tags. It could be anywhere that uses the trait. (Although we're only using it on Tags for now)
  • Moved the trait to Statamic\Support\Traits\Hookable (mimicking Macroable)
  • Renamed addHook/runHook to hook/runHooks.

@jasonvarga jasonvarga merged commit 8b98f00 into statamic:4.x Feb 23, 2024
18 checks passed
@ryanmitchell ryanmitchell deleted the feature/add-tag-setup-hook branch February 27, 2024 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants