Auto-run task interval

User
we have this task scheduler NOw i want to give intervval in package setting page and want concrete auto run this at that interval
Assistant
Got it — you want your **`generate_merchant_feed`** Task to auto‑run on a user‑defined interval from your package settings page.

Here’s a clean, Concrete CMS 9–style way to do it:

# 1) Keep your task registered (you already did)

Your `tasks.xml`/CIF is fine:

<concrete5-cif version="1.0">
  <tasks>
    <task handle="generate_merchant_feed" package="shopping_feed_manager"/>
  </tasks>
  <tasksets>
    <taskset handle="community_store">
      <task handle="generate_merchant_feed"/>
    </taskset>
  </tasksets>
</concrete5-cif>

# 2) Add a “Run every N minutes” field in your package settings page

Store it in config, e.g. `shopping_feed_manager::interval_minutes`.

Example controller for your dashboard Single Page (pseudo‑paths shown):

// packages/shopping_feed_manager/controllers/single_page/dashboard/shopping_feed/settings.php
namespace ConcretePackageShoppingFeedManagerControllerSinglePageDashboardShoppingFeed;

use ConcreteCorePageControllerDashboardPageController;
use ConcreteCoreSupportFacadeConfig;
use ConcreteCoreHttpResponseFactoryInterface;
use ConcreteCoreErrorUserMessageException;
use ConcreteCoreValidationCSRFToken;

class Settings extends DashboardPageController
{
    public function view()
    {
        $this->set('interval', (int) Config::get('shopping_feed_manager::interval_minutes', 30));
    }

    public function save()
    {
        /** @var Token $token */
        $token = $this->app->make(Token::class);
        if (!$token->validate('sfm_settings')) {
            throw new UserMessageException(t('Invalid token.'));
        }

        $minutes = (int) $this->request->request->get('interval');
        if ($minutes < 1) {
            $minutes = 1; // minimum 1 minute (or set your own floor)
        }

        Config::save('shopping_feed_manager::interval_minutes', $minutes);

        // Update the schedule immediately when saving:
        $this->app->make(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)
            ->createOrUpdateSchedule($minutes);

        $this->flash('success', t('Settings saved. Task schedule updated to every %s minute(s).', $minutes));
        return $this->buildRedirect($this->action('view'));
    }
}

Simple view:

// packages/shopping_feed_manager/single_pages/dashboard/shopping_feed/settings.php
defined('C5_EXECUTE') or die('Access Denied.');
/** @var ConcreteCoreFormServiceForm $form */
$form = app('helper/form');
$token = app('token');
?>
<form method="post" action="<?=$view->action('save');?>">
  <?=$token->output('sfm_settings');?>

  <div class="mb-3">
    <label class="form-label"><?=t('Run every (minutes)');?></label>
    <?=$form->number('interval', $interval, ['min' => 1, 'step' => 1]);?>
    <div class="form-text"><?=t('How frequently to run the Google Merchant feed generator.');?></div>
  </div>

  <button type="submit" class="btn btn-primary"><?=t('Save');?></button>
</form>

# 3) Programmatically create/update the **On‑Schedule Trigger**

Create a tiny helper class to (a) find your task, (b) delete any old schedule triggers for it, (c) make a new “every N minutes” schedule, and (d) persist it.

// packages/shopping_feed_manager/src/Scheduler/FeedScheduler.php
namespace ConcretePackageShoppingFeedManagerScheduler;

use ConcreteCoreApplicationApplicationAwareInterface;
use ConcreteCoreApplicationApplicationAwareTrait;
use ConcreteCoreEntityAutomationTask;
use ConcreteCoreEntityAutomationTriggerOnScheduleTrigger;
use ConcreteCoreEntityManagerEntityManager;
use ConcreteCoreSupportFacadeApplication;
use ConcreteCoreAutomationTaskTaskFactory;
use ConcreteCoreAutomationScheduleScheduleFactory;
use ConcreteCoreAutomationTriggerOnScheduleTriggerFactory;

class FeedScheduler implements ApplicationAwareInterface
{
    use ApplicationAwareTrait;

    protected EntityManager $em;
    protected TaskFactory $taskFactory;
    protected ScheduleFactory $scheduleFactory;
    protected OnScheduleTriggerFactory $triggerFactory;

    public function __construct(
        EntityManager $em,
        TaskFactory $taskFactory,
        ScheduleFactory $scheduleFactory,
        OnScheduleTriggerFactory $triggerFactory
    ) {
        $this->em = $em;
        $this->taskFactory = $taskFactory;
        $this->scheduleFactory = $scheduleFactory;
        $this->triggerFactory = $triggerFactory;
    }

    public function createOrUpdateSchedule(int $minutes): void
    {
        // 1) Get the Task by handle
        /** @var Task|null $task */
        $task = $this->taskFactory->getByHandle('generate_merchant_feed');
        if (!$task) {
            // If the task isn’t installed yet, bail out silently or throw.
            return;
        }

        // 2) Remove existing schedule triggers for this task
        $repo = $this->em->getRepository(OnScheduleTrigger::class);
        $existing = $repo->findBy(['task' => $task]);
        foreach ($existing as $trigger) {
            $this->em->remove($trigger);
        }
        $this->em->flush();

        // 3) Create a fresh schedule: every N minutes
        // (Concrete CMS 9 provides helpers like createEveryXMinutes)
        $schedule = $this->scheduleFactory->createEveryXMinutes($minutes);

        // 4) Create and persist the trigger (links schedule + task)
        $trigger = $this->triggerFactory->create($schedule, $task);
        $this->em->persist($trigger);
        $this->em->flush();
    }
}

> Notes:
> – Class names here follow Concrete v9 Automation APIs. If your exact namespaces differ in your install, adjust imports (the idea is the same: **Task** + **Schedule** + **OnScheduleTrigger**).
> – If you keep multiple schedules (e.g., one per site), remove the delete loop and add logic to update or add per context.

Wire the class into the container (optional; Concrete auto‑resolves many):

// packages/shopping_feed_manager/controller.php (Package class)
public function on_start()
{
    $this->app->bind(
        ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class,
        static function ($app) {
            return new ConcretePackageShoppingFeedManagerSchedulerFeedScheduler(
                $app->make('DoctrineORMEntityManager'),
                $app->make(ConcreteCoreAutomationTaskTaskFactory::class),
                $app->make(ConcreteCoreAutomationScheduleScheduleFactory::class),
                $app->make(ConcreteCoreAutomationTriggerOnScheduleTriggerFactory::class),
            );
        }
    );
}

During **install/upgrade**, seed a default interval and schedule:

public function install()
{
    $pkg = parent::install();
    Config::save('shopping_feed_manager::interval_minutes', 30);
    $this->app->make(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)
        ->createOrUpdateSchedule(30);
}

public function upgrade()
{
    parent::upgrade();
    $minutes = (int) Config::get('shopping_feed_manager::interval_minutes', 30);
    $this->app->make(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)
        ->createOrUpdateSchedule($minutes);
}

# 4) Make sure the server actually *runs* the due schedules

Concrete’s Automation runner needs to be invoked periodically. Set a cron to run **every minute**:

* * * * * /path/to/php /path/to/concrete/bin/concrete automation:run >/dev/null 2>&1

– This command finds due **On‑Schedule** triggers (like the one you just created) and executes the associated Task.
– Your task controller (e.g., `CommandTaskControllerGenerateMerchantFeedController`) should focus purely on doing the work; the **schedule** handles the *when*.

# 5) (Optional) Show current next run in your settings UI

You can read back the `OnScheduleTrigger` for your task and display the schedule description / next fire time to reassure admins that it’s set correctly.

## Summary

– Store interval in config via your settings page.
– On save (and on install/upgrade), **create or update an On‑Schedule trigger** for your `generate_merchant_feed` task with “every N minutes”.
– Ensure a system cron runs `automation:run` once per minute.

If you want, paste your Task Controller class here and I’ll double‑check it’s compatible with Automation (input/output, logging, queue usage).

User
Assistant
Here’s an updated Concrete CMS 9–style view with an **interval (minutes)** setting added. It keeps your existing fields, adds a number input, and disables the interval field unless “Enable Cron Run” is checked.

<?php defined('C5_EXECUTE') or die("Access Denied.");

use ConcreteCoreFormServiceForm;
use ConcreteCoreValidationCSRFToken;

/** @var bool   $notifyOnError */
/** @var bool   $enableCron */
/** @var string $notifyEmail */
/** @var int    $interval        Minutes; provide from controller, default fallback below */
/** @var string $scheduleSummary Optional string like "Every 30 minutes" or "Next run: ..." */

$form  = app(Form::class);
$token = app(Token::class);

$interval = isset($interval) ? (int) $interval : 30;
?>

<div class="ccm-dashboard-content">
    <div class="row">
        <div class="col-12">
            <div class="card shadow-sm">
                <div class="card-header d-flex align-items-center justify-content-between">
                    <h5 class="mb-0"><?= t('General Settings') ?></h5>

                    <?php if (!empty($scheduleSummary)) { ?>
                        <span class="text-muted small">
                            <i class="fas fa-clock me-1"></i><?= h($scheduleSummary) ?>
                        </span>
                    <?php } ?>
                </div>

                <div class="card-body">
                    <form method="post" action="<?= $this->action('save') ?>" class="ccm-dashboard-form">
                        <?= $token->output('save') ?>

                        <div class="row g-3">
                            <div class="col-md-6">
                                <div class="form-check">
                                    <input class="form-check-input" type="checkbox" name="notify_on_error" value="1" id="notify_on_error"
                                           <?= $notifyOnError ? 'checked' : '' ?>>
                                    <label class="form-check-label" for="notify_on_error">
                                        <?= t('Notify me on error') ?>
                                    </label>
                                </div>
                                <div class="form-text"><?= t('Receive email notifications when feed generation fails.') ?></div>
                            </div>

                            <div class="col-md-6">
                                <div class="form-check">
                                    <input class="form-check-input js-toggle-cron" type="checkbox" name="enable_cron" value="1" id="enable_cron"
                                           <?= $enableCron ? 'checked' : '' ?>>
                                    <label class="form-check-label" for="enable_cron">
                                        <?= t('Enable Cron Run') ?>
                                    </label>
                                </div>
                                <div class="form-text"><?= t('Automatically regenerate feeds on schedule.') ?></div>
                            </div>

                            <div class="col-md-6">
                                <label for="notify_email" class="form-label"><?= t('Notification Email') ?></label>
                                <input type="email"
                                       class="form-control"
                                       name="notify_email"
                                       id="notify_email"
                                       value="<?= h($notifyEmail) ?>"
                                       placeholder="admin@example.com">
                                <div class="form-text"><?= t('Email address for error notifications.') ?></div>
                            </div>

                            <!-- New: Interval (minutes) -->
                            <div class="col-md-6">
                                <label for="interval" class="form-label"><?= t('Run every (minutes)') ?></label>
                                <input type="number"
                                       class="form-control js-interval"
                                       name="interval"
                                       id="interval"
                                       min="1"
                                       step="1"
                                       value="<?= (int) $interval ?>"
                                       <?= $enableCron ? '' : 'disabled' ?>>
                                <div class="form-text">
                                    <?= t('How frequently to run the feed generator. Minimum 1 minute.') ?>
                                </div>
                            </div>
                        </div>

                        <div class="ccm-dashboard-form-actions-wrapper mt-4">
                            <div class="ccm-dashboard-form-actions">
                                <a href="<?= h((string) ConcreteCoreSupportFacadeUrl::to('/dashboard/shopping_feed')) ?>" class="btn btn-secondary">
                                    <?= t('Cancel') ?>
                                </a>
                                <button class="btn btn-primary float-end" type="submit">
                                    <?= t('Save Settings') ?>
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>

            <script>
                (function () {
                    const toggle = document.querySelector('.js-toggle-cron');
                    const interval = document.querySelector('.js-interval');

                    function sync() {
                        if (!toggle || !interval) return;
                        interval.disabled = !toggle.checked;
                    }

                    if (toggle) {
                        toggle.addEventListener('change', sync);
                        // initial state
                        sync();
                    }
                })();
            </script>
        </div>
    </div>
</div>

### Controller notes (for completeness)
– Ensure your controller sets `$interval` (int) and optional `$scheduleSummary` for the header.
– On `save`, read `interval` (floor at 1), persist to config, and call your scheduler helper to create/update the **On‑Schedule** trigger.

User
NOw create feedScheduler
Assistant
Here’s a ready‑to‑drop‑in **FeedScheduler** for Concrete CMS 9 that:

– Enables/disables the schedule for your `generate_merchant_feed` task
– Updates the interval (every N minutes)
– Cleans up old schedule triggers
– Can return a friendly summary (“Every 30 minutes”, etc.)

Create this file:

`packages/shopping_feed_manager/src/Scheduler/FeedScheduler.php`

<?php
declare(strict_types=1);

namespace ConcretePackageShoppingFeedManagerScheduler;

use ConcreteCoreApplicationApplicationAwareInterface;
use ConcreteCoreApplicationApplicationAwareTrait;
use ConcreteCoreErrorUserMessageException;
use ConcreteCoreUtilityServiceText;
use DoctrineORMEntityManager;
use ConcreteCoreAutomationTaskTaskFactory;
use ConcreteCoreAutomationScheduleScheduleFactory;
use ConcreteCoreAutomationTriggerOnScheduleTriggerFactory;
use ConcreteCoreEntityAutomationTask;
use ConcreteCoreEntityAutomationTriggerOnScheduleTrigger;

class FeedScheduler implements ApplicationAwareInterface
{
    use ApplicationAwareTrait;

    /** @var EntityManager */
    protected $em;

    /** @var TaskFactory */
    protected $taskFactory;

    /** @var ScheduleFactory */
    protected $scheduleFactory;

    /** @var OnScheduleTriggerFactory */
    protected $triggerFactory;

    public function __construct(
        EntityManager $em,
        TaskFactory $taskFactory,
        ScheduleFactory $scheduleFactory,
        OnScheduleTriggerFactory $triggerFactory
    ) {
        $this->em = $em;
        $this->taskFactory = $taskFactory;
        $this->scheduleFactory = $scheduleFactory;
        $this->triggerFactory = $triggerFactory;
    }

    /**
     * Create/Update the on-schedule trigger for the feed task.
     *
     * @param bool $enabled  Whether the schedule should be active
     * @param int  $minutes  Interval in minutes (min 1 if enabled)
     */
    public function createOrUpdateSchedule(bool $enabled, int $minutes = 30): void
    {
        /** @var Task|null $task */
        $task = $this->taskFactory->getByHandle('generate_merchant_feed');
        if (!$task) {
            // Task not installed – nothing to schedule.
            throw new UserMessageException(t('The task "generate_merchant_feed" is not installed.'));
        }

        // Remove existing schedule triggers for this task
        $repo = $this->em->getRepository(OnScheduleTrigger::class);
        $existing = $repo->findBy(['task' => $task]);
        foreach ($existing as $trigger) {
            $this->em->remove($trigger);
        }
        $this->em->flush();

        if (!$enabled) {
            // User disabled cron – just return after cleanup.
            return;
        }

        $minutes = max(1, (int) $minutes);

        // Create a new "every N minutes" schedule
        $schedule = $this->scheduleFactory->createEveryXMinutes($minutes);

        // Link schedule to task via an on-schedule trigger
        $trigger = $this->triggerFactory->create($schedule, $task);

        $this->em->persist($trigger);
        $this->em->flush();
    }

    /**
     * Returns a short human string for the current schedule, or null if none.
     * Example: "Every 30 minutes"
     */
    public function getScheduleSummary(): ?string
    {
        /** @var Task|null $task */
        $task = $this->taskFactory->getByHandle('generate_merchant_feed');
        if (!$task) {
            return null;
        }

        $repo = $this->em->getRepository(OnScheduleTrigger::class);
        /** @var OnScheduleTrigger[] $triggers */
        $triggers = $repo->findBy(['task' => $task], ['id' => 'DESC']);

        if (!count($triggers)) {
            return null;
        }

        // We only create one; take the newest just in case
        $trigger = $triggers[0];
        $schedule = $trigger->getSchedule();
        if (!$schedule) {
            return null;
        }

        // Most Concrete schedules expose a description
        if (method_exists($schedule, 'getDescription')) {
            $desc = (string) $schedule->getDescription();
            if ($desc) {
                return $desc;
            }
        }

        // Fallback: inspect common schedule types
        // Many schedules store the interval; try to read it
        if (method_exists($schedule, 'getMinutes')) {
            $m = (int) $schedule->getMinutes();
            if ($m > 0) {
                return t('Every %s minute(s)', $m);
            }
        }

        return t('Scheduled');
    }
}

### (Optional) Bind in your package `on_start` (helps with IDE/type‑safe DI)
In `packages/shopping_feed_manager/controller.php` inside your Package class:

public function on_start()
{
    $this->app->bind(
        ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class,
        static function ($app) {
            return new ConcretePackageShoppingFeedManagerSchedulerFeedScheduler(
                $app->make('DoctrineORMEntityManager'),
                $app->make(ConcreteCoreAutomationTaskTaskFactory::class),
                $app->make(ConcreteCoreAutomationScheduleScheduleFactory::class),
                $app->make(ConcreteCoreAutomationTriggerOnScheduleTriggerFactory::class),
            );
        }
    );
}

### How to call it (from your Dashboard controller `save()`):

// After validating and saving settings:
$enableCron = (bool) $this->request->request->get('enable_cron');
$interval   = (int) $this->request->request->get('interval');

$this->app->make(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)
    ->createOrUpdateSchedule($enableCron, $interval);

### Seed on install/upgrade:

public function install()
{
    $pkg = parent::install();
    Config::save('shopping_feed_manager::interval_minutes', 30);
    Config::save('shopping_feed_manager::enable_cron', true);
    $this->app->make(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)
        ->createOrUpdateSchedule(true, 30);
}

public function upgrade()
{
    parent::upgrade();
    $enabled = (bool) Config::get('shopping_feed_manager::enable_cron', true);
    $minutes = (int) Config::get('shopping_feed_manager::interval_minutes', 30);
    $this->app->make(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)
        ->createOrUpdateSchedule($enabled, $minutes);
}

> Don’t forget the system cron:
>

> * * * * * /path/to/php /path/to/concrete/bin/concrete automation:run >/dev/null 2>&1
>

If you want this to also **pause** runs while keeping the last schedule record, we can switch from “delete & recreate” to “toggle enabled” — but for most cases, the above clean replace is simplest and avoids duplicates.

User
HOw to get internval form setting
Assistant
In Concrete CMS, the cleanest way to keep and retrieve your interval (and other settings) is through the **Config repository**.
Since in your Dashboard `save()` you’re already saving values, you can just read them back wherever you need.

### 1. Saving the interval in your Dashboard controller

public function save()
{
    $token = $this->app->make('token');
    if (!$token->validate('save')) {
        $this->error->add($token->getErrorMessage());
        return $this->view();
    }

    $interval   = max(1, (int) $this->request->request->get('interval'));
    $enableCron = (bool) $this->request->request->get('enable_cron');
    $notifyOnError = (bool) $this->request->request->get('notify_on_error');
    $notifyEmail   = trim((string) $this->request->request->get('notify_email'));

    // Save into config
    Config::save('shopping_feed_manager::interval_minutes', $interval);
    Config::save('shopping_feed_manager::enable_cron', $enableCron);
    Config::save('shopping_feed_manager::notify_on_error', $notifyOnError);
    Config::save('shopping_feed_manager::notify_email', $notifyEmail);

    // Update the automation schedule
    $this->app->make(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)
        ->createOrUpdateSchedule($enableCron, $interval);

    $this->flash('success', t('Settings saved.'));
    return $this->buildRedirect($this->action('view'));
}

### 2. Loading the interval into your Dashboard `view()`

public function view()
{
    $this->set('interval', (int) Config::get('shopping_feed_manager::interval_minutes', 30));
    $this->set('enableCron', (bool) Config::get('shopping_feed_manager::enable_cron', true));
    $this->set('notifyOnError', (bool) Config::get('shopping_feed_manager::notify_on_error', false));
    $this->set('notifyEmail', (string) Config::get('shopping_feed_manager::notify_email', ''));
}

### 3. Getting the interval anywhere else (e.g. in a service or the Task itself)

$minutes = (int) Config::get('shopping_feed_manager::interval_minutes', 30);

This returns the stored interval (default 30 if nothing saved).

✅ **Summary:**
– Save with `Config::save('shopping_feed_manager::interval_minutes', $value)`.
– Read with `Config::get('shopping_feed_manager::interval_minutes', $default)`.

That way your `FeedScheduler` or even the Task itself can always fetch the user-defined interval.

Do you also want me to show how to **auto-inject the interval into FeedScheduler** (so it doesn’t need the controller to pass it each time)?

User
in feedschedule
Assistant
Got it — here’s how to make **FeedScheduler** read the interval (and enable flag) directly from your saved settings, without needing the controller to pass them in.

### Drop‑in version (reads from Config)

<?php
declare(strict_types=1);

namespace ConcretePackageShoppingFeedManagerScheduler;

use DoctrineORMEntityManager;
use ConcreteCoreAutomationTaskTaskFactory;
use ConcreteCoreAutomationScheduleScheduleFactory;
use ConcreteCoreAutomationTriggerOnScheduleTriggerFactory;
use ConcreteCoreEntityAutomationTask;
use ConcreteCoreEntityAutomationTriggerOnScheduleTrigger;
use ConcreteCoreErrorUserMessageException;

class FeedScheduler
{
    public function __construct(
        protected EntityManager $em,
        protected TaskFactory $taskFactory,
        protected ScheduleFactory $scheduleFactory,
        protected OnScheduleTriggerFactory $triggerFactory
    ) {}

    /** Read settings saved by your dashboard page */
    protected function getSettings(): array
    {
        $enabled  = (bool) Config::get('shopping_feed_manager::enable_cron', true);
        $minutes  = (int)  Config::get('shopping_feed_manager::interval_minutes', 30);
        $minutes  = max(1, $minutes); // safety floor
        return ['enabled' => $enabled, 'minutes' => $minutes];
    }

    /**
     * One‑liner to sync the DB triggers with the current settings.
     * Call this after install/upgrade/save() or from a job/command if you want.
     */
    public function syncFromConfig(): void
    {
        $s = $this->getSettings();
        $this->createOrUpdateSchedule($s['enabled'], $s['minutes']);
    }

    /**
     * Create/Update the on‑schedule trigger for the task.
     * You can still call this directly if you want to override config.
     */
    public function createOrUpdateSchedule(bool $enabled, int $minutes = 30): void
    {
        /** @var Task|null $task */
        $task = $this->taskFactory->getByHandle('generate_merchant_feed');
        if (!$task) {
            throw new UserMessageException(t('The task "generate_merchant_feed" is not installed.'));
        }

        // Remove any existing schedule triggers for this task
        $repo = $this->em->getRepository(OnScheduleTrigger::class);
        foreach ($repo->findBy(['task' => $task]) as $trigger) {
            $this->em->remove($trigger);
        }
        $this->em->flush();

        if (!$enabled) {
            // User disabled scheduling — we’re done after cleanup
            return;
        }

        $minutes = max(1, (int) $minutes);
        $schedule = $this->scheduleFactory->createEveryXMinutes($minutes);
        $trigger  = $this->triggerFactory->create($schedule, $task);

        $this->em->persist($trigger);
        $this->em->flush();
    }

    /** Optional: show “Every N minutes” based on the current trigger */
    public function getScheduleSummary(): ?string
    {
        $task = $this->taskFactory->getByHandle('generate_merchant_feed');
        if (!$task) {
            return null;
        }
        $repo = $this->em->getRepository(OnScheduleTrigger::class);
        $trigger = $repo->findOneBy(['task' => $task], ['id' => 'DESC']);
        if (!$trigger) {
            return null;
        }
        $schedule = $trigger->getSchedule();
        if (!$schedule) {
            return null;
        }
        // Prefer schedule description if available
        if (method_exists($schedule, 'getDescription')) {
            $desc = (string) $schedule->getDescription();
            if ($desc) return $desc;
        }
        if (method_exists($schedule, 'getMinutes')) {
            return t('Every %s minute(s)', (int) $schedule->getMinutes());
        }
        return t('Scheduled');
    }
}

### How to use it

– From your dashboard controller `save()` (simple & explicit):

$this->app->make(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)
    ->syncFromConfig();

– From `install()`/`upgrade()` (seed or resync after package changes):

Config::save('shopping_feed_manager::interval_minutes', 30);
Config::save('shopping_feed_manager::enable_cron', true);

$this->app->make(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)
    ->syncFromConfig();

Now the scheduler **pulls the interval straight from your settings** whenever you call `syncFromConfig()`. If you still want to override on the fly, you can call `createOrUpdateSchedule($enabled, $minutes)` directly.

User
HOw to include it in controller.php
Assistant
Here’s exactly what to add to your **package controller** to wire in `FeedScheduler`.

### 1) Make sure your `src/` is autoloaded
If you’re not using Composer in the package, add this to your controller:

// packages/shopping_feed_manager/controller.php
namespace ConcretePackageShoppingFeedManager;

use ConcreteCorePackagePackage;

class Controller extends Package
{
    protected $pkgHandle = 'shopping_feed_manager';
    protected $appVersionRequired = '9.0.0';
    protected $pkgVersion = '1.0.0';

    // 🔸 Autoload /src
    public function getPackageAutoloaderRegistries()
    {
        return [
            'src' => 'ConcretePackageShoppingFeedManager',
        ];
    }

    // ...
}

### 2) Import the classes you’ll call

use ConcretePackageShoppingFeedManagerSchedulerFeedScheduler;
use ConcreteCoreSupportFacadeConfig;

### 3) (Optional) Bind the scheduler in `on_start`
Concrete can auto‑resolve, but binding is nice for IDEs and clarity:

public function on_start()
{
    $this->app->bind(FeedScheduler::class, function ($app) {
        return new FeedScheduler(
            $app->make('DoctrineORMEntityManager'),
            $app->make(ConcreteCoreAutomationTaskTaskFactory::class),
            $app->make(ConcreteCoreAutomationScheduleScheduleFactory::class),
            $app->make(ConcreteCoreAutomationTriggerOnScheduleTriggerFactory::class),
        );
    });
}

### 4) Call it during install / upgrade
Seed defaults (if not set) and sync the schedule from config.

public function install()
{
    $pkg = parent::install();

    // seed defaults (only if you want initial values)
    if (Config::get('shopping_feed_manager::interval_minutes') === null) {
        Config::save('shopping_feed_manager::interval_minutes', 30);
    }
    if (Config::get('shopping_feed_manager::enable_cron') === null) {
        Config::save('shopping_feed_manager::enable_cron', true);
    }

    // create/update the Automation trigger from current config
    $this->app->make(FeedScheduler::class)->syncFromConfig();
}

public function upgrade()
{
    parent::upgrade();

    // keep schedule in sync after upgrades
    $this->app->make(FeedScheduler::class)->syncFromConfig();
}

### 5) (Optional) Helper you can reuse anywhere
If you like, add a tiny helper method to your controller to resync:

protected function resyncFeedSchedule(): void
{
    $this->app->make(FeedScheduler::class)->syncFromConfig();
}

Then you can call `$this->resyncFeedSchedule();` from tasks, migrations, etc.

That’s it. With this in place, your Dashboard `save()` just updates config; calling `FeedScheduler::syncFromConfig()` (e.g., right after saving) will recreate the **On‑Schedule** trigger with the latest **Enable** + **Interval** settings.

User
I want it to list ion /dashboard/system/automation/schedule in concrete cms 9
Assistant
To have your feed’s schedule show up under **Dashboard → System & Settings → Automation → Schedule** (`/dashboard/system/automation/schedule`) you just need to make sure you’re actually **creating & persisting an On‑Schedule trigger** (with its Schedule) for your task. Once that trigger exists in the DB and is enabled, Concrete CMS 9 lists it there automatically.

Here’s a drop‑in tweak to your `FeedScheduler` that guarantees the trigger is created, enabled, and visible in that screen:

// packages/shopping_feed_manager/src/Scheduler/FeedScheduler.php
namespace ConcretePackageShoppingFeedManagerScheduler;

use DoctrineORMEntityManager;
use ConcreteCoreAutomationTaskTaskFactory;
use ConcreteCoreAutomationScheduleScheduleFactory;
use ConcreteCoreAutomationTriggerOnScheduleTriggerFactory;
use ConcreteCoreEntityAutomationTask;
use ConcreteCoreEntityAutomationTriggerOnScheduleTrigger;
use ConcreteCoreErrorUserMessageException;

class FeedScheduler
{
    public function __construct(
        protected EntityManager $em,
        protected TaskFactory $taskFactory,
        protected ScheduleFactory $scheduleFactory,
        protected OnScheduleTriggerFactory $triggerFactory
    ) {}

    public function syncFromConfig(): void
    {
        $enabled = (bool) Config::get('shopping_feed_manager::enable_cron', true);
        $minutes = max(1, (int) Config::get('shopping_feed_manager::interval_minutes', 30));
        $this->createOrUpdateSchedule($enabled, $minutes);
    }

    public function createOrUpdateSchedule(bool $enabled, int $minutes): void
    {
        /** @var Task|null $task */
        $task = $this->taskFactory->getByHandle('generate_merchant_feed');
        if (!$task) {
            throw new UserMessageException(t('The task "generate_merchant_feed" is not installed.'));
        }

        // Remove old on-schedule triggers for this task so we keep exactly one visible entry
        $repo = $this->em->getRepository(OnScheduleTrigger::class);
        foreach ($repo->findBy(['task' => $task]) as $old) {
            $this->em->remove($old);
        }
        $this->em->flush();

        if (!$enabled) {
            // Disabled: no trigger -> nothing will appear in the list
            return;
        }

        // Create schedule & trigger (these are the entities that the UI lists)
        $minutes = max(1, (int) $minutes);
        $schedule = $this->scheduleFactory->createEveryXMinutes($minutes);

        // Optional: if your Schedule object supports description/notes, set it for nicer UI
        if (method_exists($schedule, 'setDescription')) {
            $schedule->setDescription(t('Every %s minute(s) — Google Merchant feed', $minutes));
        } elseif (method_exists($schedule, 'setNotes')) {
            $schedule->setNotes(t('Every %s minute(s) — Google Merchant feed', $minutes));
        }

        // Persist schedule explicitly to be safe
        $this->em->persist($schedule);

        // Link schedule <-> task via OnSchedule trigger
        $trigger = $this->triggerFactory->create($schedule, $task);

        // Make sure the trigger is enabled so it shows and runs
        if (method_exists($trigger, 'setIsEnabled')) {
            $trigger->setIsEnabled(true);
        }

        $this->em->persist($trigger);
        $this->em->flush();
    }
}

### Key points to show it in the UI
– You **must** have an **`OnScheduleTrigger`** entity persisted and (ideally) enabled.
– The **Schedule** entity should be persisted (done above).
– If you **disable** scheduling in your settings, we delete the trigger — then it won’t appear (by design). If you’d rather keep it visible but disabled, don’t delete; instead set `setIsEnabled(false)` on the trigger.

### If you prefer “keep but disabled” (still visible)
Replace the delete‑and‑recreate logic with “find or make, then toggle enabled + update schedule interval”:

$trigger = $repo->findOneBy(['task' => $task]);
if (!$trigger) {
    $schedule = $this->scheduleFactory->createEveryXMinutes($minutes);
    $this->em->persist($schedule);
    $trigger = $this->triggerFactory->create($schedule, $task);
    $this->em->persist($trigger);
} else {
    // Update the existing trigger’s schedule to new interval
    $schedule = $trigger->getSchedule();
    if (method_exists($schedule, 'setMinutes')) {
        $schedule->setMinutes($minutes);
    } else {
        // fallback: replace schedule entity
        $this->em->remove($schedule);
        $schedule = $this->scheduleFactory->createEveryXMinutes($minutes);
        $this->em->persist($schedule);
        // If trigger has a setter, reattach
        if (method_exists($trigger, 'setSchedule')) {
            $trigger->setSchedule($schedule);
        }
    }
}
if (method_exists($trigger, 'setIsEnabled')) {
    $trigger->setIsEnabled($enabled);
}
$this->em->flush();

### Don’t forget to call `syncFromConfig()`
– After saving settings (in your dashboard controller)
– On package `install()` / `upgrade()`

Once this runs with `enable_cron = true`, you’ll see an **On‑Schedule** entry for your task on `/dashboard/system/automation/schedule`.

User
Target class [ConcreteCoreAutomationTaskTaskFactory] does not exist.
when added this in on_Start
$scheduler = $this->app->make(FeedScheduler::class);
$scheduler->syncFromConfig();
Assistant
Gotcha — the error means Concrete CMS 9 doesn’t provide `ConcreteCoreAutomationTaskTaskFactory` on your install. Easiest fix: **don’t depend on TaskFactory at all**. Look up the Task via Doctrine directly.

Here’s a drop‑in update.

# 1) Update `FeedScheduler` to use Doctrine only

`packages/shopping_feed_manager/src/Scheduler/FeedScheduler.php`

<?php
declare(strict_types=1);

namespace ConcretePackageShoppingFeedManagerScheduler;

use DoctrineORMEntityManager;
use ConcreteCoreAutomationScheduleScheduleFactory;
use ConcreteCoreAutomationTriggerOnScheduleTriggerFactory;
use ConcreteCoreEntityAutomationTask;
use ConcreteCoreEntityAutomationTriggerOnScheduleTrigger;
use ConcreteCoreErrorUserMessageException;

class FeedScheduler
{
    public function __construct(
        protected EntityManager $em,
        protected ScheduleFactory $scheduleFactory,
        protected OnScheduleTriggerFactory $triggerFactory
    ) {}

    /** Read settings from config */
    protected function getSettings(): array
    {
        $enabled = (bool) Config::get('shopping_feed_manager::enable_cron', true);
        $minutes = max(1, (int) Config::get('shopping_feed_manager::interval_minutes', 30));
        return ['enabled' => $enabled, 'minutes' => $minutes];
    }

    /** Call this after install/upgrade/save() */
    public function syncFromConfig(): void
    {
        $s = $this->getSettings();
        $this->createOrUpdateSchedule($s['enabled'], $s['minutes']);
    }

    public function createOrUpdateSchedule(bool $enabled, int $minutes): void
    {
        // ✅ Get Task via Doctrine (no TaskFactory)
        $taskRepo = $this->em->getRepository(Task::class);
        /** @var Task|null $task */
        $task = $taskRepo->findOneBy(['handle' => 'generate_merchant_feed']);
        if (!$task) {
            throw new UserMessageException(t('The task "generate_merchant_feed" is not installed.'));
        }

        // Remove existing schedule triggers for a single clean entry
        $triggerRepo = $this->em->getRepository(OnScheduleTrigger::class);
        foreach ($triggerRepo->findBy(['task' => $task]) as $old) {
            $this->em->remove($old);
        }
        $this->em->flush();

        if (!$enabled) {
            // keep it off the list if disabled; change strategy if you prefer "disabled but visible"
            return;
        }

        // Create “every N minutes” schedule + trigger
        $minutes = max(1, (int) $minutes);
        $schedule = $this->scheduleFactory->createEveryXMinutes($minutes);
        if (method_exists($schedule, 'setDescription')) {
            $schedule->setDescription(t('Every %s minute(s) — Google Merchant feed', $minutes));
        }

        $this->em->persist($schedule);

        $trigger = $this->triggerFactory->create($schedule, $task);
        if (method_exists($trigger, 'setIsEnabled')) {
            $trigger->setIsEnabled(true);
        }

        $this->em->persist($trigger);
        $this->em->flush();
    }
}

# 2) Fix your package controller binding

Since we removed `TaskFactory`, update the binding to only inject what we actually use:

// packages/shopping_feed_manager/controller.php
namespace ConcretePackageShoppingFeedManager;

use ConcreteCorePackagePackage;
use ConcretePackageShoppingFeedManagerSchedulerFeedScheduler;

class Controller extends Package
{
    protected $pkgHandle = 'shopping_feed_manager';
    protected $appVersionRequired = '9.0.0';
    protected $pkgVersion = '1.0.0';

    public function getPackageAutoloaderRegistries()
    {
        return ['src' => 'ConcretePackageShoppingFeedManager'];
    }

    public function on_start()
    {
        // Bind (optional, Concrete can auto‑resolve too)
        $this->app->bind(FeedScheduler::class, function ($app) {
            return new FeedScheduler(
                $app->make('DoctrineORMEntityManager'),
                $app->make(ConcreteCoreAutomationScheduleScheduleFactory::class),
                $app->make(ConcreteCoreAutomationTriggerOnScheduleTriggerFactory::class),
            );
        });

        // If you want to sync on every request, keep this (usually better to call after save/install/upgrade)
        // $this->app->make(FeedScheduler::class)->syncFromConfig();
    }

    public function install()
    {
        $pkg = parent::install();
        Config::save('shopping_feed_manager::interval_minutes', 30);
        Config::save('shopping_feed_manager::enable_cron', true);
        $this->app->make(FeedScheduler::class)->syncFromConfig();
    }

    public function upgrade()
    {
        parent::upgrade();
        $this->app->make(FeedScheduler::class)->syncFromConfig();
    }
}

# 3) After you save settings
From your dashboard controller `save()` (unchanged):

$this->app->make(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)
    ->syncFromConfig();

# 4) Confirm it shows in Automation → Schedule
With `enable_cron = true`, you should now see a single **On‑Schedule** entry for your task at `/dashboard/system/automation/schedule`. If you want it to remain visible but disabled, don’t delete old triggers; instead set `setIsEnabled(false)` on the existing one.

# 5) Cron runner
Make sure your system cron runs Concrete’s automation:

* * * * * /path/to/php /path/to/concrete/bin/concrete automation:run >/dev/null 2>&1

That’s it — no `TaskFactory` needed, and your schedule should list correctly.

User
Target class [ConcretePackageShoppingFeedManagerSchedulerFeedScheduler] does not exist.
Assistant
This error means Concrete can’t autoload your class. Let’s fix the autoload wiring and the usual gotchas.

# Quick checklist

1) **File path & name**
– Create the file exactly here (case‑sensitive on Linux):

packages/shopping_feed_manager/src/Scheduler/FeedScheduler.php

– File contents must start with this namespace:

<?php
declare(strict_types=1);

namespace ConcretePackageShoppingFeedManagerScheduler;

use DoctrineORMEntityManager;
use ConcreteCoreAutomationScheduleScheduleFactory;
use ConcreteCoreAutomationTriggerOnScheduleTriggerFactory;
use ConcreteCoreEntityAutomationTask;
use ConcreteCoreEntityAutomationTriggerOnScheduleTrigger;
use ConcreteCoreErrorUserMessageException;

class FeedScheduler
{
    public function __construct(
        protected EntityManager $em,
        protected ScheduleFactory $scheduleFactory,
        protected OnScheduleTriggerFactory $triggerFactory
    ) {}

    // ... methods from earlier (syncFromConfig, createOrUpdateSchedule, etc.)
}

– Class name must be `FeedScheduler` and the filename must be `FeedScheduler.php` (exact case).

2) **Package controller autoload registry**
Add this to your package controller (`packages/shopping_feed_manager/controller.php`):

namespace ConcretePackageShoppingFeedManager;

use ConcreteCorePackagePackage;

class Controller extends Package
{
    protected $pkgHandle = 'shopping_feed_manager';
    protected $appVersionRequired = '9.0.0';
    protected $pkgVersion = '1.0.0';

    // Make /src PSR-4 map to ConcretePackageShoppingFeedManager
    public function getPackageAutoloaderRegistries()
    {
        return [
            'src' => 'ConcretePackageShoppingFeedManager',
        ];
    }

    // ...
}

3) **Use the fully qualified class or import it**
In `controller.php`, either import:

use ConcretePackageShoppingFeedManagerSchedulerFeedScheduler;

and call:

$this->app->make(FeedScheduler::class)->syncFromConfig();

OR call with the FQCN as a string (no import needed):

$this->app->make('ConcretePackageShoppingFeedManagerSchedulerFeedScheduler')->syncFromConfig();

4) **Clear caches**
After adding the file/namespace mapping:
– Dashboard → System & Settings → Optimization → Clear Cache, or
– CLI: `php concrete/bin/concrete c5:clear-cache`

5) **Common pitfalls**
– Folder name must match **package handle**: `shopping_feed_manager` (all lower‑case, underscores).
– Namespace must be `ConcretePackageShoppingFeedManager…` (CamelCase of your package class name, not the handle).
– Your package controller’s PHP namespace is `ConcretePackageShoppingFeedManager`, so your `/src` classes must also start with that.
– Linux is case‑sensitive. `src/Scheduler/FeedScheduler.php` ≠ `src/scheduler/feedscheduler.php`.
– If you also have a `composer.json` autoload for the package, ensure it’s not conflicting. (Most packages just use `getPackageAutoloaderRegistries()`—that’s fine.)

6) **Sanity test**
Temporarily add this near the top of `on_start()` to verify autoload:

if (!class_exists(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)) {
    Log::addError('FeedScheduler class not found by autoloader.');
}

Then clear cache and reload any page; check Dashboard → Reports → Logs.

### Minimal working `on_start()` sample

public function on_start()
{
    // (Optional) You can bind, but not required:
    // $this->app->bind(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class, function ($app) {
    //     return new ConcretePackageShoppingFeedManagerSchedulerFeedScheduler(
    //         $app->make('DoctrineORMEntityManager'),
    //         $app->make(ConcreteCoreAutomationScheduleScheduleFactory::class),
    //         $app->make(ConcreteCoreAutomationTriggerOnScheduleTriggerFactory::class),
    //     );
    // });

    // Try making it (autoload must resolve now)
    $this->app
        ->make('ConcretePackageShoppingFeedManagerSchedulerFeedScheduler')
        ->syncFromConfig();
}

Do the 6 steps above (especially the autoloader registry + exact file path), clear cache, and the error will go away.

User
Target class [ConcreteCoreAutomationScheduleScheduleFactory] does not exist.
Assistant
You’re on a Concrete CMS 9 build where the **Automation factories** aren’t registered (or don’t exist). No problem — skip the factories and create the **Schedule** + **OnScheduleTrigger** entities directly.

Below is a drop‑in `FeedScheduler` that:

– Uses **Doctrine only** (no `ScheduleFactory`, no `TaskFactory`)
– Creates a schedule entity compatible with what your build exposes (tries multiple known classes)
– Persists a single **On‑Schedule** trigger so it shows under **System → Automation → Schedule**

### `packages/shopping_feed_manager/src/Scheduler/FeedScheduler.php`

<?php
declare(strict_types=1);

namespace ConcretePackageShoppingFeedManagerScheduler;

use DoctrineORMEntityManager;
use ConcreteCoreEntityAutomationTriggerOnScheduleTrigger;
use ConcreteCoreEntityAutomationTask;
use ConcreteCoreErrorUserMessageException;

class FeedScheduler
{
    public function __construct(protected EntityManager $em) {}

    /** Read current settings */
    protected function getSettings(): array
    {
        $enabled = (bool) Config::get('shopping_feed_manager::enable_cron', true);
        $minutes = max(1, (int) Config::get('shopping_feed_manager::interval_minutes', 30));
        return ['enabled' => $enabled, 'minutes' => $minutes];
    }

    /** Call this after install/upgrade/dashboard save */
    public function syncFromConfig(): void
    {
        $s = $this->getSettings();
        $this->createOrUpdateSchedule($s['enabled'], $s['minutes']);
    }

    /**
     * Create/Update the on-schedule trigger so it appears in
     * /dashboard/system/automation/schedule and actually runs.
     */
    public function createOrUpdateSchedule(bool $enabled, int $minutes): void
    {
        // 1) Find your task
        $task = $this->em->getRepository(Task::class)->findOneBy(['handle' => 'generate_merchant_feed']);
        if (!$task) {
            throw new UserMessageException(t('The task "generate_merchant_feed" is not installed.'));
        }

        // 2) Keep exactly one trigger for a clean UI
        $triggerRepo = $this->em->getRepository(OnScheduleTrigger::class);
        foreach ($triggerRepo->findBy(['task' => $task]) as $old) {
            $this->em->remove($old);
        }
        $this->em->flush();

        if (!$enabled) {
            // If disabled, don't create a trigger (or change to a disabled trigger if you prefer it visible)
            return;
        }

        $minutes = max(1, (int) $minutes);

        // 3) Build a schedule entity compatible with your Concrete version
        $schedule = $this->makeScheduleEntity($minutes);

        // Optional: decorate with a description if supported
        foreach (['setDescription', 'setNotes'] as $setter) {
            if (method_exists($schedule, $setter)) {
                $schedule->{$setter}(t('Every %s minute(s) — Google Merchant feed', $minutes));
                break;
            }
        }

        // 4) Persist schedule + trigger
        $this->em->persist($schedule);

        $trigger = new OnScheduleTrigger();
        // These setters exist in all v9 builds:
        $trigger->setSchedule($schedule);
        $trigger->setTask($task);
        if (method_exists($trigger, 'setIsEnabled')) {
            $trigger->setIsEnabled(true);
        }

        $this->em->persist($trigger);
        $this->em->flush();
    }

    /**
     * Creates an appropriate Schedule entity depending on what your build provides.
     * Tries a few known classes; falls back to a cron-expression style schedule.
     */
    protected function makeScheduleEntity(int $minutes): object
    {
        // a) Every-X-Minutes schedule (preferred if available)
        $classEvery = 'ConcreteCoreEntityAutomationScheduleEveryXMinutesSchedule';
        if (class_exists($classEvery)) {
            $schedule = new $classEvery();
            if (method_exists($schedule, 'setMinutes')) {
                $schedule->setMinutes($minutes);
                return $schedule;
            }
        }

        // b) Cron-like schedule
        $classCron = 'ConcreteCoreEntityAutomationScheduleCronSchedule';
        if (class_exists($classCron)) {
            $schedule = new $classCron();
            $expr = "*/{$minutes} * * * *";
            foreach (['setCronExpression', 'setExpression'] as $setter) {
                if (method_exists($schedule, $setter)) {
                    $schedule->{$setter}($expr);
                    return $schedule;
                }
            }
        }

        // c) Basic schedule with expression
        $classBasic = 'ConcreteCoreEntityAutomationScheduleBasicSchedule';
        if (class_exists($classBasic)) {
            $schedule = new $classBasic();
            $expr = "*/{$minutes} * * * *";
            foreach (['setCronExpression', 'setExpression'] as $setter) {
                if (method_exists($schedule, $setter)) {
                    $schedule->{$setter}($expr);
                    return $schedule;
                }
            }
        }

        // If none of the above exist, fail loudly so we notice
        throw new UserMessageException(t('No compatible schedule entity found in this Concrete CMS build.'));
    }

    /** Optional: read back a short description for your UI */
    public function getScheduleSummary(): ?string
    {
        $task = $this->em->getRepository(Task::class)->findOneBy(['handle' => 'generate_merchant_feed']);
        if (!$task) {
            return null;
        }
        $trigger = $this->em->getRepository(OnScheduleTrigger::class)->findOneBy(['task' => $task], ['id' => 'DESC']);
        if (!$trigger) {
            return null;
        }
        $schedule = $trigger->getSchedule();
        if (!$schedule) {
            return null;
        }
        if (method_exists($schedule, 'getDescription')) {
            $desc = (string) $schedule->getDescription();
            if ($desc) return $desc;
        }
        if (method_exists($schedule, 'getMinutes')) {
            return t('Every %s minute(s)', (int) $schedule->getMinutes());
        }
        return t('Scheduled');
    }
}

### Update your package controller

You no longer need to resolve `ScheduleFactory`. Bind nothing, or just make the class directly:

// packages/shopping_feed_manager/controller.php
namespace ConcretePackageShoppingFeedManager;

use ConcreteCorePackagePackage;

class Controller extends Package
{
    protected $pkgHandle = 'shopping_feed_manager';
    protected $appVersionRequired = '9.0.0';
    protected $pkgVersion = '1.0.0';

    public function getPackageAutoloaderRegistries()
    {
        return ['src' => 'ConcretePackageShoppingFeedManager'];
    }

    public function install()
    {
        $pkg = parent::install();
        Config::save('shopping_feed_manager::interval_minutes', 30);
        Config::save('shopping_feed_manager::enable_cron', true);

        // Make the schedule now
        $this->app->make(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)->syncFromConfig();
    }

    public function upgrade()
    {
        parent::upgrade();
        $this->app->make(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)->syncFromConfig();
    }

    // If you were calling it in on_start() and want to keep that, this works too:
    // public function on_start()
    // {
    //     $this->app->make(ConcretePackageShoppingFeedManagerSchedulerFeedScheduler::class)->syncFromConfig();
    // }
}

### Don’t forget

– **Clear cache** after adding the class and autoloader mapping:
Dashboard → System & Settings → Optimization → Clear Cache (or `php concrete/bin/concrete c5:clear-cache`)
– Ensure your system cron runs the automation runner:

* * * * * /path/to/php /path/to/concrete/bin/concrete automation:run >/dev/null 2>&1

– With `enable_cron = true`, you should now see your entry under
**/dashboard/system/automation/schedule**.

If your build still errors on the schedule class, tell me your exact Concrete CMS version (e.g., 9.0.2 / 9.1 / 9.2.x) and I’ll tailor the schedule entity detection to that version.

Leave a Comment

Your email address will not be published. Required fields are marked *