Concrete CMS scheduling fix

User
Noting in feedshcedler now
Class 'ConcreteCoreEntityAutomationTriggerOnScheduleTrigger' does not exist
Assistant
Got it 👍

That error tells me you’re trying to use `OnScheduleTrigger`, but in **Concrete CMS 9** the automation API classes have changed — there is **no**
`ConcreteCoreEntityAutomationTriggerOnScheduleTrigger`.

Here’s the correct mapping for scheduling tasks in Concrete CMS 9:

– **Tasks** live under `ConcreteCoreCommandTask`
– **Schedules** live under `ConcreteCoreAutomationSchedule`
– The default “Run on Schedule” trigger entity is
`ConcreteCoreEntityAutomationSchedule`.

So instead of `OnScheduleTrigger`, you should use the **Schedule entity** and **ScheduleFactory**.

Example for registering a scheduled task in your package’s `on_start` or `install()`:

use ConcreteCoreAutomationTaskTaskFactory;
use ConcreteCoreAutomationScheduleScheduleFactory;
use ConcreteCoreEntityAutomationSchedule;

public function on_start()
{
    $app = $this->app;

    // Get the task factory
    $taskFactory = $app->make(TaskFactory::class);

    // Example: find your task (registered via tasks.xml or manually)
    $task = $taskFactory->getByHandle('generate_merchant_feed');

    if ($task) {
        // Get schedule factory
        $scheduleFactory = $app->make(ScheduleFactory::class);

        // Create or update a schedule for this task
        $schedule = $scheduleFactory->createFromTask($task);

        // Example: run every day at midnight
        $schedule->setCronExpression('0 0 * * *');
        $schedule->setIsEnabled(true);

        $em = $app->make('database/orm')->entityManager();
        $em->persist($schedule);
        $em->flush();
    }
}

### Key Points:
– Replace `OnScheduleTrigger` with `ConcreteCoreEntityAutomationSchedule`.
– Use `ScheduleFactory` to create schedules tied to tasks.
– Schedules will then show up in **Dashboard → System & Settings → Automation → Schedules**.

👉 Question: Do you want me to show you how to **define the task itself** (with `tasks.xml` + Task Controller), or just how to attach your existing task to the schedule UI?

User
so there is no need to FeedScheduler
Assistant
Exactly ✅

You don’t need to build a separate **`FeedScheduler`** class anymore.

Concrete CMS 9 already gives you:

– **TaskFactory** → to register/find your task (from `tasks.xml` or code).
– **ScheduleFactory** → to create schedules for that task.
– **Schedule entity** → which Concrete uses to store the cron expression, enabled/disabled state, etc.

So instead of maintaining a custom `FeedScheduler`, you just:

1. Define your task (e.g. `generate_merchant_feed`) in `tasks.xml` and its controller.
2. Use `ScheduleFactory` in your package (install or on_start) to create the schedule.
3. Then it automatically shows up in **Dashboard → System & Settings → Automation → Schedules**.

👉 In other words:
– **Old Concrete (8.x)** → you’d create a custom scheduler/trigger class.
– **Concrete 9+** → everything is standardized with `Schedule`, so you don’t reinvent it.

Do you want me to write you a **full minimal working example** (task + schedule) so you can drop it in your package and see it appear in the dashboard?

User
SO give code here
User
Concrete 9+ → everything is standardized with Schedule, so you don’t reinvent it
SO give me code here
Assistant
Awesome—here’s a **minimal working example** for Concrete CMS 9+ that:

– defines a custom task (`generate_merchant_feed`)
– wires a controller + command + handler
– registers the task in `tasks.xml`
– **creates a schedule in PHP** (cron-style: daily at 00:00)

I’m using the **official v9 task APIs**: `ConcreteCoreCommandTask*` for tasks, and `ConcreteCoreCommandSchedulerScheduler` to create scheduled tasks programmatically. (Docs for tasks & the scheduler are here: citeturn12view0turn14view0turn10view0turn13search0)

### 1) `packages/merchant_feed/controller.php` (package controller)

<?php
namespace ConcretePackageMerchantFeed;

use ConcreteCorePackagePackage;
use ConcreteCoreCommandTaskManager as TaskManager;
use ConcreteCoreCommandTaskTaskService;
use ConcreteCoreCommandTaskInputInputFactory;
use ConcreteCoreCommandSchedulerScheduler;
use ConcretePackageMerchantFeedCommandTaskControllerGenerateMerchantFeedController;

defined('C5_EXECUTE') or die('Access Denied.');

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

    public function getPackageName()
    {
        return t('Merchant Feed');
    }

    public function getPackageDescription()
    {
        return t('Generates a merchant product feed on a schedule.');
    }

    // PSR-4 autoload for your src/ folder
    public function getPackageAutoloaderRegistries()
    {
        return [
            'src' => 'ConcretePackageMerchantFeed',
        ];
    }

    public function install()
    {
        $pkg = parent::install();
        // Install task via content XML
        $this->installContentFile('tasks.xml');

        // After task exists, create a schedule (e.g., every day at midnight)
        $this->createDailySchedule('generate_merchant_feed', '0 0 * * *');
    }

    public function upgrade()
    {
        parent::upgrade();
        $this->installContentFile('tasks.xml');
    }

    public function on_start()
    {
        // Register the task controller with the Task Manager
        $manager = $this->app->make(TaskManager::class);
        $manager->extend('generate_merchant_feed', function () {
            return new GenerateMerchantFeedController();
        });
    }

    /**
     * Helper: create a scheduled task (if not already scheduled)
     */
    protected function createDailySchedule(string $handle, string $cron)
    {
        $app = $this->app;

        /** @var TaskService $taskService */
        $taskService = $app->make(TaskService::class);
        $task = $taskService->getByHandle($handle);
        if (!$task) {
            return; // task not installed yet
        }

        /** @var InputFactory $inputFactory */
        $inputFactory = $app->make(InputFactory::class);
        $input = $inputFactory->createFromArray([]); // add task inputs here if you have any

        /** @var Scheduler $scheduler */
        $scheduler = $app->make(Scheduler::class);
        // Creates (or updates) a scheduled task entry tied to this Task + Input + CRON
        $scheduler->createScheduledTask($task, $input, $cron);
    }
}

> Why these classes?
> – **TaskService**: find tasks by handle (core v9 API). citeturn3view0
> – **InputFactory**: build a `InputInterface` for the task run. citeturn11search5
> – **Scheduler**: programmatically create a schedule (UI uses same system). citeturn10view0

### 2) `packages/merchant_feed/tasks.xml` (register the task)

<?xml version="1.0" encoding="UTF-8"?>
<concrete5-cif version="1.0">
  <tasks>
    <!-- Handle must match what you extend() in on_start -->
    <task handle="generate_merchant_feed" package="merchant_feed"/>
  </tasks>
  <!-- Optional: put your task in a task set -->
  <tasksets>
    <taskset handle="maintenance">
      <task handle="generate_merchant_feed"/>
    </taskset>
  </tasksets>
</concrete5-cif>

(Tasks XML pattern per docs. citeturn12view0)

### 3) Task Controller
`packages/merchant_feed/src/Command/Task/Controller/GenerateMerchantFeedController.php`

<?php
namespace ConcretePackageMerchantFeedCommandTaskController;

use ConcreteCoreCommandTaskControllerAbstractController;
use ConcreteCoreCommandTaskInputInputInterface;
use ConcreteCoreCommandTaskRunnerCommandTaskRunner;
use ConcreteCoreCommandTaskRunnerTaskRunnerInterface;
use ConcreteCoreCommandTaskTaskInterface;
use ConcretePackageMerchantFeedFeedCommandGenerateMerchantFeedCommand;

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

class GenerateMerchantFeedController extends AbstractController
{
    public function getName(): string
    {
        return t('Generate Merchant Feed');
    }

    public function getDescription(): string
    {
        return t('Builds and stores the merchant product feed.');
    }

    public function getTaskRunner(TaskInterface $task, InputInterface $input): TaskRunnerInterface
    {
        // Kick off a simple command immediately when the task runs
        $command = new GenerateMerchantFeedCommand();
        return new CommandTaskRunner($task, $command, t('Merchant feed generated.'));
    }
}

(Task runner approach per docs; here we use `CommandTaskRunner` for a simple, synchronous run. citeturn14view0)

### 4) Command + Handler

`packages/merchant_feed/src/Feed/Command/GenerateMerchantFeedCommand.php`

<?php
namespace ConcretePackageMerchantFeedFeedCommand;

use ConcreteCoreFoundationCommandCommand;

class GenerateMerchantFeedCommand extends Command
{
    // Add properties for inputs if you need them (e.g., channel, locale, etc.)
}

`packages/merchant_feed/src/Feed/Command/GenerateMerchantFeedCommandHandler.php`

<?php
namespace ConcretePackageMerchantFeedFeedCommand;

use ConcreteCoreSupportFacadeLog;
use ConcreteCoreFileServiceFile as FileService;

class GenerateMerchantFeedCommandHandler
{
    public function __invoke(GenerateMerchantFeedCommand $command)
    {
        // 1) Build your feed data (query products, etc.)
        $csv = "id,title,pricen";
        $csv .= "SKU-001,Example Product,99.00n";

        // 2) Save it somewhere public or in /application/files
        $fh = app(FileService::class);
        $path = DIR_BASE . '/application/files/merchant_feed.csv';
        file_put_contents($path, $csv);

        Log::info(sprintf('[merchant_feed] Wrote feed: %s', $path));
    }
}

> The handler’s `__invoke()` is where your business logic lives; this pattern is straight from the v9 task docs. citeturn14view0

### 5) Enable scheduling on the server (one‑time)

To let dashboard/UI (and programmatic) schedules actually run, **add this cron** (every minute) on your server:

# cPanel-style example:
*/1 * * * * /usr/local/bin/php -q /path/to/site/public/concrete/bin/concrete concrete:scheduler:run >> /dev/null 2>&1

(Concrete’s scheduler daemon reads your schedules and triggers tasks on time.) citeturn13search0

## Notes & gotchas

– The “factory” you want in v9 is **TaskService** (not `TaskFactory`). It exposes `getByHandle()` etc. citeturn3view0
– Programmatic scheduling uses **`ConcreteCoreCommandSchedulerScheduler::createScheduledTask()`**, passing your `TaskInterface`, an `InputInterface`, and a cron expression. citeturn10view0
– Schedules appear in **Dashboard → System & Settings → Automation → Schedule** once created. citeturn1search3

If you want it to run more often (e.g., every 15 min), change the cron to `*/15 * * * *`. (Reference cron examples if needed. citeturn0search20)

If you paste these files into a `merchant_feed` package and install it, you’ll see the task in **Dashboard → System & Settings → Automation → Tasks**, and the schedule will be created during install (provided the task exists).

User
Attempted to call an undefined method named "createFromArray" of class "ConcreteCoreCommandTaskInputInputFactory".
Assistant
You're right—`InputFactory::createFromArray()` doesn’t exist in Concrete 9. Use a plain `Input` object instead.

Here’s the fixed bit you can drop into your package controller (replacing the broken code):

use ConcreteCoreCommandTaskTaskService;
use ConcreteCoreCommandTaskInputInput;               // ✅ use Input directly
use ConcreteCoreCommandSchedulerScheduler;

protected function createDailySchedule(string $handle, string $cron)
{
    $app = $this->app;

    /** @var TaskService $tasks */
    $tasks = $app->make(TaskService::class);
    $task = $tasks->getByHandle($handle);
    if (!$task) {
        return; // task not installed yet
    }

    // No inputs? Just pass an empty Input instance.
    $input = new Input();                                  // ✅ instead of InputFactory::createFromArray([])

    /** @var Scheduler $scheduler */
    $scheduler = $app->make(Scheduler::class);
    $scheduler->createScheduledTask($task, $input, $cron); // e.g. '0 0 * * *' for daily at midnight
}

If your task needs parameters later, define them via `getInputDefinition()` in the task controller and the UI/CLI will supply them; for programmatic schedules you can still stick with `new Input()` unless you must pre-fill values. The docs show using `new Input()` directly in task runners and explain input definitions. citeturn9view0

Also, ensure the scheduler daemon is set up on the server (one cron entry that runs every minute) so UI/programmatic schedules actually fire. citeturn10search1

Want me to patch the earlier full example with this correction applied end‑to‑end?

User
HOw to set interval from package setting page
Assistant
Gotcha—you want the **schedule interval to be editable from your package’s Settings page**, and when saved it should **update the Concrete v9 scheduler** for your task.

Below is a clean, minimal setup you can drop into your package.

# 1) Create a Dashboard “Settings” page

**File:** `packages/shopping_feed_manager/controllers/single_page/dashboard/shopping_feed/settings.php`

<?php
namespace ConcretePackageShoppingFeedManagerControllerSinglePageDashboardShoppingFeed;

use ConcreteCorePageControllerDashboardPageController;
use ConcreteCoreSupportFacadeConfig;
use ConcreteCoreValidationCSRFToken;
use CronCronExpression; // comes with Concrete; used for validation
use ConcretePackageShoppingFeedManagerSchedulerSchedulerSync;

defined('C5_EXECUTE') or die('Access Denied.');

class Settings extends DashboardPageController
{
    public function view()
    {
        $this->set('cron', (string) Config::get('shopping_feed_manager.cron', '0 0 * * *')); // default daily at midnight
        $this->set('message', null);
        $this->set('error', null);
    }

    public function save()
    {
        /** @var Token $token */
        $token = $this->app->make(Token::class);
        if (!$token->validate('sfm_settings_save')) {
            $this->error->add(t('Invalid security token. Please reload the page and try again.'));
            return $this->view();
        }

        $cron = trim((string) $this->post('cron'));
        if (!$cron) {
            $this->error->add(t('Please provide a CRON expression.'));
            return $this->view();
        }

        if (!CronExpression::isValidExpression($cron)) {
            $this->error->add(t('The CRON expression is invalid.'));
            return $this->view();
        }

        // Save to config
        Config::save('shopping_feed_manager.cron', $cron);

        // Re-sync the Concrete scheduler for our task
        /** @var SchedulerSync $sync */
        $sync = $this->app->make(SchedulerSync::class);
        $sync->syncFromConfig($cron);

        $this->set('message', t('Settings saved. Schedule updated to "%s".', $cron));
        $this->view();
    }
}

**File:** `packages/shopping_feed_manager/single_pages/dashboard/shopping_feed/settings.php`

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

/** @var string $cron */
/** @var ConcreteCoreValidationCSRFToken $token */
$app = ConcreteCoreSupportFacadeApplication::getFacadeApplication();
$form = $app->make('helper/form');
$token = $app->make('token');
?>

<div class="ccm-dashboard-content">
  <?php if (isset($message) && $message) { ?>
    <div class="alert alert-success"><?= h($message) ?></div>
  <?php } ?>
  <?php if (isset($error) && $error && $error->has()) { ?>
    <div class="alert alert-danger"><?= $error->output(); ?></div>
  <?php } ?>

  <form method="post" action="<?= $view->action('save'); ?>" class="row gy-3">
    <?php $token->output('sfm_settings_save'); ?>

    <div class="col-12 col-md-6">
      <label class="form-label"><?= t('CRON Expression'); ?></label>
      <?= $form->text('cron', $cron, ['placeholder' => 'e.g. */15 * * * *']); ?>
      <div class="form-text">
        <?= t('Examples:'); ?>
        <code>*/15 * * * * </code> (<?= t('every 15 min'); ?>),
        <code>0 * * * *</code> (<?= t('hourly'); ?>),
        <code>0 0 * * *</code> (<?= t('daily'); ?>)
      </div>
    </div>

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

**Register the single page on install** (if not already):

// packages/shopping_feed_manager/controller.php (inside install())
use ConcreteCorePageSingle as SinglePage;
...
public function install()
{
    $pkg = parent::install();
    // Tasks, etc.
    $this->installContentFile('tasks.xml');

    // Dashboard page
    $sp = SinglePage::add('/dashboard/shopping_feed/settings', $pkg);
    if ($sp) {
        $sp->update(['cName' => t('Feed Settings')]);
    }
}

# 2) The Scheduler Sync class

This class reads the saved CRON (or uses what you pass) and **updates the Concrete scheduler** for your task.

**File:** `packages/shopping_feed_manager/src/Scheduler/SchedulerSync.php`

<?php
namespace ConcretePackageShoppingFeedManagerScheduler;

use ConcreteCoreSupportFacadeConfig;
use ConcreteCoreCommandTaskTaskService;
use ConcreteCoreCommandTaskInputInput;
use ConcreteCoreCommandSchedulerScheduler;
use DoctrineORMEntityManagerInterface;

class SchedulerSync
{
    protected TaskService $tasks;
    protected Scheduler $scheduler;
    protected EntityManagerInterface $em;

    // Handle of the task you registered in tasks.xml and on_start
    protected string $taskHandle = 'generate_merchant_feed';

    public function __construct(TaskService $tasks, Scheduler $scheduler, EntityManagerInterface $em)
    {
        $this->tasks     = $tasks;
        $this->scheduler = $scheduler;
        $this->em        = $em;
    }

    /**
     * Sync the scheduled task with the current config (or an override).
     */
    public function syncFromConfig(?string $cronOverride = null): void
    {
        $task = $this->tasks->getByHandle($this->taskHandle);
        if (!$task) {
            return;
        }

        $cron = $cronOverride ?? (string) Config::get('shopping_feed_manager.cron', '0 0 * * *');

        // Remove existing schedules for this task (keep it simple to avoid duplicates)
        $this->deleteExistingSchedulesForTask($task->getID());

        // Recreate schedule with new CRON
        $input = new Input(); // add inputs if your task needs them
        $this->scheduler->createScheduledTask($task, $input, $cron);
        $this->em->flush();
    }

    /**
     * Delete existing scheduled entries for a task (prevents duplicates when changing CRON).
     */
    protected function deleteExistingSchedulesForTask(int $taskId): void
    {
        // ScheduledTask entity exists in Concrete v9 under Command namespace
        $repo = $this->em->getRepository(ConcreteCoreEntityCommandScheduledTask::class);

        // Find all schedules for this task
        $existing = $repo->findBy(['task' => $taskId]);
        foreach ($existing as $schedule) {
            $this->em->remove($schedule);
        }
    }
}

> Notes:
> – We **delete existing schedules** before creating a new one so you don’t end up with duplicates when the user changes the interval.
> – If your Concrete build uses a slightly different namespace for the entity, adjust `ConcreteCoreEntityCommandScheduledTask` accordingly (this class name is correct for Concrete 9.x).
> – If your task needs inputs, replace `new Input()` with an `Input` that has your values (and define them in your Task Controller’s `getInputDefinition()`).

# 3) Call sync on install/upgrade (and optional on_start)

In your **package controller**:

use ConcretePackageShoppingFeedManagerSchedulerSchedulerSync;
use ConcreteCoreSupportFacadeConfig;

public function install()
{
    $pkg = parent::install();
    $this->installContentFile('tasks.xml');

    // Default cron if not set
    if (!Config::has('shopping_feed_manager.cron')) {
        Config::save('shopping_feed_manager.cron', '0 0 * * *'); // daily 00:00
    }

    // Ensure a schedule exists using current config
    $this->app->make(SchedulerSync::class)->syncFromConfig();
}

public function upgrade()
{
    parent::upgrade();
    $this->installContentFile('tasks.xml');
    $this->app->make(SchedulerSync::class)->syncFromConfig();
}

(You don’t need to do it in `on_start()` unless you specifically want to re-sync on every request; saving from the settings page already triggers a sync.)

# 4) Make sure your task is registered

You should already have your task defined and wired (per your earlier setup):

– `tasks.xml` includes “
– In `on_start()` you extend the task handle with your Task Controller:

$manager = $this->app->make(ConcreteCoreCommandTaskManager::class);
  $manager->extend('generate_merchant_feed', function () {
      return new ConcretePackageShoppingFeedManagerCommandTaskControllerGenerateMerchantFeedController();
  });

# 5) (One-time) server cron

Ensure the Concrete **scheduler runner** is invoked every minute:

*/1 * * * * /usr/bin/php -q /path/to/site/public/concrete/bin/concrete concrete:scheduler:run >/dev/null 2>&1

## That’s it

– User edits the **CRON** on `/dashboard/shopping_feed/settings`
– Clicks **Save**
– We validate the expression, write it to config, and **rebuild the Concrete schedule** for your task

If you want preset dropdowns (Every 5/15/30 min, Hourly, Daily), just render a “ and map the choice to a cron string before saving.

User
can i call $this->createDailySchedule('generate_merchant_feed', '0 0 * * *'); this from on_start
Assistant
You **can**, but it’s not ideal to call

$this->createDailySchedule('generate_merchant_feed', '0 0 * * *');

from `on_start()`.

Here’s why:

### 🔎 How `on_start()` works in Concrete 9
– `on_start()` runs on **every page load** while your package is enabled.
– If you create schedules there, you’ll try to **re-insert the schedule on every request**, which can:
– create duplicates (unless you delete first each time),
– add unnecessary DB work on every page view.

### ✅ Best practice
– Create/update the schedule when:
1. **installing/upgrading** the package, OR
2. **saving your settings** in your dashboard page (like we did in `SchedulerSync::syncFromConfig()`).

That way the schedule only changes when you explicitly want it to.

### 👌 If you *do* want to ensure it exists in `on_start()`
You can call it safely if you guard against duplicates. Example:

public function on_start()
{
    $this->app->make(ConcretePackageShoppingFeedManagerSchedulerSchedulerSync::class)
              ->syncFromConfig();
}

This uses your `SchedulerSync` class to **delete old schedules and re-create the one** from config. That makes it idempotent (no duplicates), but it still runs on every request.

So it’s “safe”, but not the most efficient—better to only run on install/upgrade/settings save.

👉 Do you want me to show you a **hybrid pattern** where `on_start()` only ensures the schedule exists if it’s missing (instead of recreating every time)?

User
make it every hour
Assistant
Perfect 👍 If you want the feed task to run **every hour**, just change the CRON expression to:

0 * * * *

That means: *at minute 0 of every hour*.

### Example: inside your package controller

public function install()
{
    $pkg = parent::install();
    $this->installContentFile('tasks.xml');

    // Default cron: every hour
    if (!Config::has('shopping_feed_manager.cron')) {
        Config::save('shopping_feed_manager.cron', '0 * * * *');
    }

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

### Example: calling manually (if you still want a helper)

$this->createDailySchedule('generate_merchant_feed', '0 * * * *');

🔁 In this case your helper should probably be renamed to `createSchedule()` since it’s not “daily” anymore 😅

### Summary
– **Hourly cron string** → `0 * * * *`
– Use that in `Config::save(…)` or in your `createSchedule()` call.
– The rest of your scheduler logic stays the same.

Want me to give you a little **ready-to-paste version of `createSchedule()`** that accepts any cron expression and only adds the schedule if it doesn’t already exist?

User
yes
Assistant
Here you go—drop this **helper** into your package controller (or a small utility class).
It accepts **any cron expression**, avoids duplicates, and (re)enables the schedule if it already exists.

use ConcreteCoreCommandTaskTaskService;
use ConcreteCoreCommandTaskInputInput;
use ConcreteCoreCommandSchedulerScheduler;
use DoctrineORMEntityManagerInterface;

/**
 * Create (or ensure) a schedule for a task.
 *
 * @param string $handle Task handle registered via tasks.xml and on_start() extend()
 * @param string $cron   Cron expression, e.g. '0 * * * *' (hourly), '0 0 * * *' (daily)
 * @param bool   $enable Whether the schedule should be enabled
 */
protected function createSchedule(string $handle, string $cron, bool $enable = true): void
{
    $app = $this->app;

    /** @var TaskService $tasks */
    $tasks = $app->make(TaskService::class);
    $task  = $tasks->getByHandle($handle);
    if (!$task) {
        return; // Task not installed/registered yet
    }

    /** @var EntityManagerInterface $em */
    $em   = $app->make('database/orm')->entityManager();
    $repo = $em->getRepository(ConcreteCoreEntityCommandScheduledTask::class);

    // Find any existing schedules for this task
    $existing = $repo->findBy(['task' => $task->getID()]);

    // If a schedule with the same cron exists, just ensure its enabled flag is correct and bail
    foreach ($existing as $schedule) {
        if (method_exists($schedule, 'getCronExpression') && $schedule->getCronExpression() === $cron) {
            if (method_exists($schedule, 'setIsEnabled')) {
                $schedule->setIsEnabled($enable);
                $em->flush();
            }
            return;
        }
    }

    // Otherwise, clean up any old schedules to avoid duplicates
    foreach ($existing as $schedule) {
        $em->remove($schedule);
    }
    $em->flush();

    // Create a fresh scheduled task
    /** @var Scheduler $scheduler */
    $scheduler = $app->make(Scheduler::class);
    $input     = new Input(); // Add inputs here if your task requires any

    // Signature supports the enabled flag as the 4th param in Concrete 9
    $scheduler->createScheduledTask($task, $input, $cron, $enable);
    $em->flush();
}

### How to use it

**Hourly:**

$this->createSchedule('generate_merchant_feed', '0 * * * *'); // every hour at :00

**Every 15 minutes:**

$this->createSchedule('generate_merchant_feed', '*/15 * * * *');

**Daily at midnight:**

$this->createSchedule('generate_merchant_feed', '0 0 * * *');

### Where to call it
– **Best:** during `install()`, `upgrade()`, and when saving your **Settings** page.
– **Avoid:** calling on every request in `on_start()` (unless you *really* need to).
If you must, it’s safe because the helper is idempotent, but it still adds DB work.

If you want, I can also show a tiny wrapper that reads the cron from `Config::get('shopping_feed_manager.cron')` and calls this helper—handy for your settings-save flow.

Leave a Comment

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