Every rich-text field rebuilds the workflow placeholder list: one query per attribute

Author: alex-frolovCreated Sep 8, 2026Updated Sep 9, 2026

Bug report

Issue Description

Every rich-text field in the admin panel costs one query per attribute in the installation, because <x-admin::tinymce> rebuilds the workflow placeholder list on each render.

Where the queries come from. packages/Webkul/Automation/src/Helpers/Entity/AbstractEntity.php:63getAttributes() walks every attribute of an entity type and reads $attribute->options, a lazy hasMany on attribute_options, so one SELECT per attribute:

php
foreach ($this->attributeRepository->findByField('entity_type', $entityType) as $attribute) {
    ...
    $options = $attribute->options;      // one query, per attribute

Entity::getEmailTemplatePlaceholders() (Entity.php:89-101) runs that for all eight entities in config('workflows.trigger_entities'), and the TinyMCE component calls it on the first line of its template (packages/Webkul/Admin/src/Resources/views/components/tinymce/index.blade.php:1):

blade
@php($placeholders = app('\Webkul\Automation\Helpers\Entity')->getEmailTemplatePlaceholders())

so the whole walk repeats for every rich-text field rendered on the page — lead views, person views, the mail composer, email templates, webhooks and workflows.

Measured (MySQL 8.0, queries per getEmailTemplatePlaceholders() call):

queries
stock installation 19
after adding 10 custom lead attributes 29

One extra query per attribute — and adding attributes is what the attribute system is for, so the cost is lowest on a fresh install and grows with real use.

Preconditions

  1. Krayin master @ 2c12091 (Core::KRAYIN_VERSION 2.2.6), also present in v2.2.5.
  2. PHP 8.3.33, Laravel 12, MySQL 8.0.

Steps to reproduce

  1. In php artisan tinker:
php
DB::enableQueryLog();
app(\Webkul\Automation\Helpers\Entity::class)->getEmailTemplatePlaceholders();
count(DB::getQueryLog());     // 19 on a fresh install
  1. Add ten custom attributes under Settings → Attributes (any entity type, type select).
  2. Repeat step 1: the count is now 29 — one more query per attribute added.
  3. Open any admin screen with a rich-text field and read Laravel Debugbar's Queries tab: the same select * from attribute_options where attribute_id = ? series appears once per editor on the page.

Expected result

  • Building the placeholder list costs a constant, small number of queries, and does not repeat for each editor on a page.

Eager-loading the options in the one place that reads them is enough — measured 10 → 2 on the leads entity type alone:

php
foreach ($this->attributeRepository->with('options')->findByField('entity_type', $entityType) as $attribute) {

Since the placeholder list is identical for every editor on a page, memoising getEmailTemplatePlaceholders() per request would remove the repetition as well.

Actual result

Each rendered rich-text field issues one query per attribute across all eight trigger entities, and the total rises by one for every attribute an admin configures.

This may be the mechanism behind #980 ("Create WorkFlow Query Way Too Much!"), which was closed as not reproducible on an empty dataset — the cost scales with configured attributes, and a fresh install has almost none.


How this was found

Found with query-guard — a PHPUnit extension that traces the SQL a test run produces, fingerprints every query by call site and shape, and flags a call site when the same-shaped query runs 3+ times with differing bound values inside one test (batched IN (...) lookups are excluded, since those are the cure rather than the disease).

The Pest suite could not surface this on its own, since these screens are covered by the Playwright suite rather than by PHPUnit. So the numbers above come from driving admin screens through Laravel's HTTP kernel — real route, middleware, controller and Blade — and then measuring the helper directly with the number of configured attributes varied.

You do not need that tool to check any of this: the reproduction above uses nothing but DB::enableQueryLog() in Tinker, and Laravel Debugbar's Queries tab shows the same series in the browser.