Problem/Motivation

Entity types without bundles or with BundlePlugin based bundles are not supported. This blocks Drupal Canvas and its Canvas Page entity type along with others.

Original

I have a custom entity type with no bundles. For what I've seen, SchedulerAdminForm needs a bundle entity type to work.
Is it possible to implement a plugin for this case ?

Steps to reproduce

For exemple, generate a new entity type with drupal console or manually, with no bundles.
Try to create a new plugin for this entity type.

Proposed resolution

1. Make plugin discovery bundle-optional.
2. Add a non-bundle settings source (entity-type-level Scheduler config).
3. Branch runtime logic in `SchedulerManager` for bundled vs no-bundle entities.
4. Add an admin UI path for no-bundle entity type Scheduler settings.
5. Add test coverage with `entity_test_no_bundle`.

Issue fork scheduler-3355087

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

rkcreation created an issue. See original summary.

apatel0325’s picture

Assigned: Unassigned » apatel0325
apatel0325’s picture

Assigned: apatel0325 » Unassigned

@rkcreation Can you please try below?

// Define your entity class.
class MyEntity extends ContentEntityBase {
// Define your entity properties and fields.
}

// Define your entity type using the ContentEntityType class.
$contentEntityType = ContentEntityType::create([
'id' => 'my_entity',
'label' => 'My Entity',
'entity_class' => MyEntity::class,
'base_table' => 'my_entity',
// Define your entity's fields and storage settings here.
]);

// Implement hook_entity_type_build() to register your entity type with Drupal.
function mymodule_entity_type_build(&$entityTypes) {
$entityTypes['my_entity'] = $contentEntityType;
}

// Define a custom entity form for your entity.
function mymodule_entity_form_display(\Drupal\Core\Entity\EntityInterface $entity, $mode, $form, &$form_state) {
if ($entity->getEntityTypeId() == 'my_entity') {
// Define your custom form display here.
}
}

// Define any additional functionality for your entity.
function mymodule_my_entity_validate(MyEntity $entity) {
// Define your custom validation logic here.
}

pacproduct’s picture

I am facing the same issue, but I'm not trying to create a custom entity myself: I wanted to use the 'ad' module which defines an 'ad_content' entity with no bundle.

I tried declaring a Scheduler plugin (with the idea to eventually suggest it as an addition to the 'ad' module), but after several tries and some xdebug sessions, I realized that the scheduler module explicitly excludes entity types that do not have any bundle (in \Drupal\scheduler\SchedulerPluginManager::findDefinitions):

    foreach ($definitions as $plugin_id => $plugin_definition) {
      [...]

      $entityType = $this->entityTypeManager->getDefinition($plugin_definition['entityType'], FALSE);
      if (!$entityType || !$entityType->getBundleEntityType()) {
        unset($definitions[$plugin_id]);
      }
    }

My Plugin definition gets unset right there.

Is there a workaround for enabling scheduling on contrib entities with no bundles?

jonathan1055’s picture

Hi rkcreation and pacproduct,

Thanks for giving your feedback here, it is useful. Currently, as you discovered, Scheduler does expect a bundle on the entity type. Scheduler 8.x-1.x only supported nodes and everything was hardcoded. This was expanded in 2.x with the entity plugin, but we did not build in from the start the possibility of using the plugin on entities without a bundle type, because it was built as a logical follow-on from nodes. Everything, all options and processing, is currently structured around entity bundle types, and there is no quick work-around.

I would be very interested to see how much work is involved in getting Scheduler to work on non-bundle entity types. It could be quite complex, or maybe it could be straight-forward if we expanded the plugin manager with new functions. My feeling is that it will be a lot of work, but that is not a reason to discard the idea.

apatel0325 - regarding your post in #2-3 above, I don't understand what you were trying to show. I think you must have mis-understood what this issue is about.

mglaman’s picture

I took a closer look at current `2.x-dev` and confirmed this is still a structural limitation, not just a missing plugin example.

Scheduler currently assumes bundle-backed entity types in several places:

  • `SchedulerPluginManager::findDefinitions()` drops plugin definitions when `getBundleEntityType()` is empty.
  • `SchedulerPluginBase` assumes bundle APIs (`typeFieldName()`, `getTypes()`, `entityTypeFormIds()`).
  • `SchedulerManager::getThirdPartySetting()` reads settings from bundle third-party settings.
  • Publish/unpublish query and logging paths also assume bundle fields/entities are available.
  • Admin UX is tied to bundle edit forms (`_scheduler_entity_type_form_alter()`), with no equivalent for no-bundle entity types.

So there is no practical workaround today for no-bundle entities (for example, `entity_test_no_bundle`) without Scheduler changes.

A feasible implementation path appears to be:

1. Make plugin discovery bundle-optional.
2. Add a non-bundle settings source (entity-type-level Scheduler config).
3. Branch runtime logic in `SchedulerManager` for bundled vs no-bundle entities.
4. Add an admin UI path for no-bundle entity type Scheduler settings.
5. Add test coverage with `entity_test_no_bundle`.

Small code examples

1) Current hard stop in plugin discovery

$entityType = $this->entityTypeManager->getDefinition($plugin_definition['entityType'], FALSE);
if (!$entityType || !$entityType->getBundleEntityType()) {
  unset($definitions[$plugin_id]);
}

2) Current third-party settings lookup assumes bundle entity

$typeFieldName = $this->getPlugin($entity->getEntityTypeId())->typeFieldName();
return $entity->$typeFieldName->entity->getThirdPartySetting('scheduler', $setting, $default);

3) Example no-bundle entity from core tests

#[ContentEntityType(
  id: 'entity_test_no_bundle',
  entity_keys: [
    'id' => 'id',
    'revision' => 'revision_id',
  ],
  base_table: 'entity_test_no_bundle',
)]
class EntityTestNoBundle extends EntityTest {}

4) Sketch: make discovery bundle-optional

$entityType = $this->entityTypeManager->getDefinition($plugin_definition['entityType'], FALSE);
if (!$entityType) {
  unset($definitions[$plugin_id]);
}

5) Sketch: branch settings lookup for no-bundle entities

$entity_type = $entity->getEntityType();
$bundle_key = $entity_type->getKey('bundle');

if ($bundle_key) {
  return $entity->{$bundle_key}->entity->getThirdPartySetting('scheduler', $setting, $default);
}

// No-bundle fallback, e.g. scheduler.entity_type_settings.{entity_type_id}
return $this->configFactory
  ->get("scheduler.entity_type_settings.{$entity->getEntityTypeId()}")
  ->get($setting) ?? $default;
mglaman’s picture

Issue summary: View changes
mglaman’s picture

Status: Active » Needs review
mglaman’s picture

Title: Support for non-bundle entities ? » Support for non-bundle entity types

kristen pol’s picture

Status: Needs review » Needs work

phpcs failure

FILE: README.md
------------------------------------------------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
------------------------------------------------------------------------------------------------------------
94 | WARNING | Line exceeds 80 characters; contains 82 characters (Drupal.Files.TxtFileLineLength.TooLong)
------------------------------------------------------------------------------------------------------------

kristen pol’s picture

Status: Needs work » Needs review
kristen pol’s picture

Status: Needs review » Needs work

Pipeline failures

mglaman’s picture

Status: Needs work » Needs review

MR is green again

scott falconer’s picture

Status: Needs review » Needs work

Found one blocking issue in my testing:
The new SchedulerNoBundleSettingsForm submit path can fatal when the entity type has a form mode registered via hook_entity_form_mode_info_alter() without a backing core.entity_form_mode.* config entity.

Steps to recreate:
- POST /admin/config/content/scheduler/entity_test_no_bundle
- Result: HTTP 500
- Watchdog:
Error: Call to a member function getConfigDependencyName() on null in Drupal\Core\Entity\EntityDisplayBase->calculateDependencies()
- Stack points back to SchedulerNoBundleSettingsForm.php around the form display save loop.

While this may not happen in vanilla paths, since this issue is about broadening Scheduler to support custom/contrib non-bundle entity types, this edge case may occur and the result is a 500 / site that is left partially configured.

Likely cause:
SchedulerNoBundleSettingsForm::submitForm() loops over modes from getFormModes() and saves an EntityFormDisplay for each one. For alter-only form modes, Core cannot load the corresponding entity_form_mode config entity during dependency calculation, so saving the display fatals.

One additional concern:
The config save happens before the form-display loop, so the failed submit can leave Scheduler partially configured even though the user sees an error page.

Disclosure: AI was used to evaluate and test the code, which was then manually verified.

mglaman’s picture

Status: Needs work » Needs review

addressed #15

kristen pol’s picture

I've requested Rakhi at this to the sprint.

rakhimandhania’s picture

Issue tags: +AI Initiative Sprint
scott falconer’s picture

I retested the current MR head. Looks good moving to rtbc.

Validation:
- Reviewed the current diff against 2.x.
- Confirmed the follow-up commit addresses the prior blocker by skipping alter-only form modes without backing entity_form_mode config, and by saving Scheduler config after the form-display loop.
- Ran SchedulerNoBundleSupportTest locally: 2 tests, 15 assertions, pass. Existing deprecations only.
- Ran a browser smoke against the no-bundle settings route and saved /admin/config/content/scheduler/entity_test_no_bundle successfully.
- Confirmed saved config and default form display state include the Scheduler components.
- Watchdog did not show the prior fatal. Only expected page-not-found noise from checking that the bundle-backed Scheduler route is rejected.

Disclosure: AI was used to help review and test this, and I reviewed the output/results before posting.

scott falconer’s picture

Status: Needs review » Reviewed & tested by the community
kristen pol’s picture

Yay! This is a stable blocker for ai_context

Who can we nudge to get this in and get a release created?

kristen pol’s picture

Oh! @jonathan1055 was here recently

🙏🙏 🙏🙏 🙏🙏

Let’s us know how we can help get this over the line

UPDATED: I pinged @jonathan1055 in the contribute channel

jonathan1055’s picture

Status: Reviewed & tested by the community » Needs work

This is excellent work, thank you. I've left a few comments in the MR, and @mglaman has already addressed some. I've not completed the review yet, but adding comments as and when I get time to do a bit more.

One request I have is with the Tugboat Preview. Currently this installs the additional modules Commerce and Media, and sets up scheduling on those. Could you extend it so that we can test a non-bundle entity in Tugboat? This could use the EntityTestNoBundleScheduler entity and plugin, or we could implement a plugin for a proper core non-bundled entity, as that would be a real-life useful example.

mglaman’s picture

Status: Needs work » Needs review

https://git.drupalcode.org/issue/scheduler-3355087/-/commit/a39229bd50ee... tries to get this working on Tugboat :) GitLab queues look backed up though, so the commit isn't in the MR yet.

jonathan1055’s picture

Status: Needs review » Needs work
StatusFileSize
new223.96 KB
new284.57 KB
new161.98 KB

Thank you. Yes that works in Tugboat. I have some feedback about the new form, see attached screen shots, easy things to change. I can do this if you like, just say.

I also have questions about structure and navigation, the other scheduler-enabled entities have a view to show the items and the scheduled items. These are missing here, but that is probably due to this just being a test entity created specifically. Other non-bundle entities would have a url to get to them, and edit them. I will push a trial implementation for a Block plugin, then we can see how it works for a real entity.

Another major omission at the moment is actual test coverage for the Scheduler operations on non-bundled entities. We really need to add a non-bundled entity into dataStandardEntityTypes() within the test SchedulerSetupTrait. Maybe we could use the Block plugin for that, so I will push that new file and then we'll go from there.

But overall this is excellent.

mglaman’s picture

Assigned: Unassigned » mglaman

Ty jonathan1055 for feedback. I'm afk today but will address on Monday

jonathan1055’s picture

StatusFileSize
new237.61 KB

Regarding Content Blocks, I think the other issue is mis-leading, or I confused myself, as these are bundled. Now that I've got the fields correct in the new plugin, we get all the Block Types showing in the dropdown menu. See attached. So I will remove the block plugin work here, and leave it for the other issue.

jonathan1055’s picture

My feedback about the visual appearance of the form is probably due to your recent work extracting the common parts of _scheduler_entity_type_form_alter() and SchedulerNoBundleSettingsForm::buildForm. I'm guilty of causing that, as I asked for that re-factoring. So for now, let's not worry about the form appearance, but concentrate on adding a non-bundled entity into the set of Scheduler functional tests. That's more important than how the form looks.

mglaman’s picture

Assigned: mglaman » Unassigned
Status: Needs work » Needs review

Oh goodness the MR is finally green! Going to review it and see if we can clean up the appearance again.

kristen pol’s picture

I just want to acknowledge the huge amount of work Matt and Jonathan have been doing on this issue! Thank you both 🙏🙏🙏

mglaman’s picture

CI ✅ (including previous major)
Tugboat ✅
UI adjustments from #25 addressed ✅

mglaman’s picture

Issue tags: +MidCamp2026
scott falconer’s picture

Status: Needs review » Reviewed & tested by the community

I reviewed the current MR head again and this looks RTBC to me.

What I checked:

- The original no-bundle settings form submit failure is covered by SchedulerNoBundleSupportTest::testSubmitFormSkipsPhantomFormMode().
- The current MR uses a Scheduler-owned no-bundle publishable test entity
The no-bundle functional slices now cover the relevant default-time handling / hidden/disabled scheduler field behavior / X-Robots/meta output / delete-with-past-dates behavior

I also re-tested the real admin form path in a DDEV browser session against the current MR head:
- /admin/config/content/scheduler/scheduler_test_no_bundle loaded
- enabling publishing/unpublishing and saving completed without the previous fatal
- the form returned to the settings page with “Scheduler settings saved.”
- saved config has publish_enable: true and unpublish_enable: true
- no watchdog errors

kristen pol’s picture

Yay! 🎉 Hope this can get in soon 🤞

jonathan1055’s picture

I have read through all the changes since the last review on 8 May. Great work Matt.

I like that you removed the dependency on drupal:entity_test I've pushed three commits, minor tweaks, no functional changes.

I want to ask though in in this commit file tests/modules/scheduler_no_bundle_test/src/Plugin/Scheduler/EntityTestNoBundleScheduler.php the class name should really be SchedulerTestNoBundleScheduler because EntityTestNoBundleScheduler is a left-over from when you were using the core EntityTest. Is it OK if that is changed? I can push that, but wanted to get your view first.

Also I notice that the Non-Bundle test entity is not used in the javascript tests, but the non-bundle Trait is added to SchedulerJavascriptTestBase.php in this commit Was there an intention to use it in the Javascript tests?

mglaman’s picture

@jonathan1055 both addressed, pushed.

Class rename. Done. EntityTestNoBundleScheduler -> SchedulerTestNoBundleScheduler. The plugin id and entityType were already scheduler_test_no_bundle_*, so only the class and file changed.

No-bundle in the JavaScript tests. Reverted. The JS tests configure Scheduler per entity type through bundle third-party settings:

$entityType = $this->entityTypeObject($entityTypeId);
$entityType->setThirdPartySetting('scheduler', $field . '_required', $required)->save();

For a no-bundle type, entityTypeObject() returns the ContentEntityType definition, which has no setThirdPartySetting(), hence the fatal. No-bundle settings live in config (entityTypeNoBundleConfig()), not on a bundle. There is also no JavaScript-specific edit-form behavior unique to no-bundle entities to assert here; the vertical-tab and default-time widgets are the same code path. Coverage stays in the Functional and Kernel tests.

I kept SchedulerNoBundleEntitySetupTrait on SchedulerJavascriptTestBase. It is not there to add JS coverage, SchedulerSetupTrait dispatches the no-bundle type to methods that the trait defines:

// SchedulerSetupTrait::createEntity()
case 'scheduler_test_no_bundle':
  $entity = $this->createEntityTestEntity($values);
  break;

Without the trait on the class, PHPStan can't resolve createEntityTestEntity() / getEntityTestEntity() in that context. Added a comment on the use to make that explicit.

phenaproxima’s picture

This is a critical issue to land for Drupal CMS, because it blocks us from getting to Drupal 11.4 unless we shim something.

jonathan1055’s picture

@mglaman Thanks for renaming the class, that's good.

Yes after pushing the addition of the non-budle test data to the javascript tests I spent a short while running them locally, and discovered exactly what you have explained, that $entityType->setThirdPartySetting() will not work here, or it would take quite a bit of effort to change. I wanted to see how the default time worked. But I have tested that manually and I think it does OK. I had to use the manual url scheduler_test_no_bundle/add because there is no navigation to add this entity. I also saved without entering a title and got wsod with

TypeError: Drupal\Component\Utility\Html::escape(): Argument #1 ($text) must be of type string, null given, called in core/lib/Drupal/Component/Render/FormattableMarkup.php on line 238 in Drupal\Component\Utility\Html::escape() (line 433 of core/lib/Drupal/Component/Utility/Html.php).

which isn't very user-friendly if you click 'save' to early. There is no way back either, because you also get that error when trying to edit. But I guess this is due to the test entity not being a "real" non-bundle entity, it is lacking in several features. Also, for example, the scheluder settings have the radio button choice of fieldset or vertical tab, but this entity does not support vertical tabs on the end page. Again I expect if we were using a "real" non-bundle entity this would be better. I'm just stating these things here, in case we need a follow-up issue to address them.

Let's move forward. I'll merge it now.

jonathan1055’s picture

Status: Reviewed & tested by the community » Fixed

I have just tagged and released Scheduler 2.3.0

Thank you to @mglaman, @kirsten-pol and @scott-falconer for making this major feature possible.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

kristen pol’s picture

Jonathan and Matt, thank you!!! 🙏

pfrenssen’s picture

Very nice work, thanks very much Jonathan and Matt!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.