Set Cron for XML Feed

User
Now we have created feed package now I want to set cron to generate xml file and store in concrete cms
Assistant
To set up a **cron job** in **Concrete CMS 9** that generates and stores an XML file (e.g., a Google Merchant feed), you should follow these steps:

## ✅ Step 1: Create a Console Command in Your Package

Inside your package directory (`packages/shopping_feed_manager/src/Command/GenerateFeedCommand.php`):

<?php
namespace ConcretePackageShoppingFeedManagerCommand;

use ConcreteCoreConsoleCommand;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputOutputInterface;

class GenerateFeedCommand extends Command
{
    protected function configure()
    {
        $this
            ->setName('shopping-feed:generate')
            ->setDescription('Generates the shopping feed XML file.');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $app = ConcreteCoreSupportFacadeApplication::getFacadeApplication();
        $db = $app->make('database')->connection();

        $output->writeln('Generating feed...');

        // Fetch feed settings and products
        $feeds = $db->fetchAll('SELECT * FROM feedurl');

        foreach ($feeds as $feed) {
            $products = $db->fetchAll('SELECT * FROM products WHERE feed_id = ?', [$feed['id']]);

            // Generate XML content
            $xml = new SimpleXMLElement('<products/>');
            foreach ($products as $product) {
                $item = $xml->addChild('product');
                $item->addChild('id', $product['id']);
                $item->addChild('title', htmlspecialchars($product['title']));
                $item->addChild('price', $product['price']);
                // Add other fields as needed
            }

            // Save to file
            $filePath = DIR_APPLICATION . "/files/feeds/feed_{$feed['id']}.xml";
            $xml->asXML($filePath);

            $output->writeln("Feed generated: feed_{$feed['id']}.xml");
        }

        $output->writeln('Done.');
        return 0;
    }
}

## ✅ Step 2: Register the Command in Package Controller

In your package `controller.php`:

use ConcreteCoreConsoleCommandCommand;
use ConcretePackageShoppingFeedManagerCommandGenerateFeedCommand;

public function on_start()
{
    if ($this->app->bound('console')) {
        $this->app->extend('console', function ($console) {
            $console->add(new GenerateFeedCommand());
            return $console;
        });
    }
}

## ✅ Step 3: Test Command via CLI

From your Concrete CMS root folder:

./vendor/bin/concrete c5:shopping-feed:generate

## ✅ Step 4: Setup the Cron Job

SSH into your server and add this to your crontab (`crontab -e`):

* * * * * /usr/bin/php /path-to-your-site/concrete/bin/concrete c5:shopping-feed:generate >> /path-to-your-site/application/files/feeds/cron.log 2>&1

⏱️ Adjust timing to your need (e.g., run every hour: `0 * * * *`)

## ✅ Step 5: Make Sure Directory is Writable

Ensure the directory `application/files/feeds/` exists and is writable:

mkdir -p application/files/feeds
chmod 775 application/files/feeds

Would you like this feed URL to be publicly accessible as well (e.g., `yourdomain.com/feeds/feed_1.xml`)? I can help you expose that too.

User
This needs to be a task. Create a task like “generate shopping feeds” in the Dashboard à System and Settings à Automation à Tasks.
This runs and can be scheduled by the ConcreteCMS standards. If a feed will be runned within this task is a setting per feed
Assistant
Perfect! To properly integrate with **Concrete CMS's Automation → Tasks system**, you’ll need to create a **custom Task Controller** within your package. This allows the "Generate Shopping Feeds" task to be managed in the Dashboard, run manually, and scheduled using Concrete’s built-in task scheduler.

### ✅ Step-by-Step: Create a Task to "Generate Shopping Feeds"

### 🗂️ 1. Create the Task Class

**Location**: `packages/shopping_feed_manager/src/Task/GeneratorTask.php`

<?php
namespace ConcretePackageShoppingFeedManagerTask;

use ConcreteCoreEntityAutomationTask;
use ConcreteCoreTaskTaskInterface;
use ConcreteCoreTaskTaskResultInterface;
use ConcreteCoreTaskTaskResultFactory as ResultFactory;
use ConcreteCoreDatabaseConnectionConnection;

class GeneratorTask implements TaskInterface
{
    protected $resultFactory;
    protected $db;

    public function __construct(ResultFactory $resultFactory, Connection $db)
    {
        $this->resultFactory = $resultFactory;
        $this->db = $db;
    }

    public function getName(): string
    {
        return t('Generate Shopping Feeds');
    }

    public function getHandle(): string
    {
        return 'generate_shopping_feeds';
    }

    public function getDescription(): string
    {
        return t('Generates XML product feeds for all active feeds.');
    }

    public function execute(Task $task): TaskResultInterface
    {
        $feeds = $this->db->fetchAll('SELECT * FROM feedurl WHERE enable_task = 1');

        foreach ($feeds as $feed) {
            $products = $this->db->fetchAll('SELECT * FROM products WHERE feed_id = ?', [$feed['id']]);

            $xml = new SimpleXMLElement('<products/>');
            foreach ($products as $product) {
                $item = $xml->addChild('product');
                $item->addChild('id', $product['id']);
                $item->addChild('title', htmlspecialchars($product['title']));
                $item->addChild('price', $product['price']);
            }

            $filePath = DIR_APPLICATION . "/files/feeds/feed_{$feed['id']}.xml";
            $xml->asXML($filePath);
        }

        return $this->resultFactory->create($task, t('Feeds generated successfully.'));
    }
}

### 🧠 Assumption

– You must have a column `enable_task` (boolean) in your `feedurl` table to decide per feed whether to generate it.

### 🧷 2. Bind the Task in Your Package Controller

In `controller.php` of your package:

use ConcreteCoreTaskManager as TaskManager;
use ConcretePackageShoppingFeedManagerTaskGeneratorTask;
use ConcreteCoreTaskTaskResultFactory as ResultFactory;
use ConcreteCoreDatabaseConnectionConnection;

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

    // Bind the Task so Concrete can register it
    $app->extend(TaskManager::class, function (TaskManager $manager) use ($app) {
        $manager->registerTask(
            'generate_shopping_feeds',
            function () use ($app) {
                return new GeneratorTask(
                    $app->make(ResultFactory::class),
                    $app->make(Connection::class)
                );
            }
        );
        return $manager;
    });
}

### ✅ 3. Install the Task

After installing or upgrading the package, go to:

**Dashboard → System & Settings → Automation → Tasks → Add Task**

– Click "Add Task"
– Find “Generate Shopping Feeds”
– Add it
– You can now **run it manually** or **schedule it** with automation rules

### 🧪 4. Test the Task

After adding, click the play icon to **run it manually**.

Then check:

– XML files in `/application/files/feeds/`
– Output log if needed
– Products and feeds processed

### ➕ Optional: Add Task Settings Per Feed in Dashboard

Let me know if you want to allow users to toggle `enable_task` from a dashboard form per feed, and I’ll help you build that part too.

Would you like to include logs or errors in the XML generation per feed, or expose the feed URL publicly via a route (e.g., `/feeds/{id}.xml`)?

User
Okay how to create global function for count number of product in feed
Assistant
To create a **global helper function** in Concrete CMS (v9+) to **count the number of products in a specific feed**, you have a few clean and Concrete-native options.

## ✅ Option 1: Use a Service Class (Recommended in Concrete CMS 9+)

Concrete encourages using services over raw global functions.

### 🗂️ Step 1: Create a Service Class

**Location**: `packages/shopping_feed_manager/src/Service/FeedService.php`

<?php
namespace ConcretePackageShoppingFeedManagerService;

use ConcreteCoreDatabaseConnectionConnection;

class FeedService
{
    protected $db;

    public function __construct(Connection $db)
    {
        $this->db = $db;
    }

    public function countProductsInFeed($feedId): int
    {
        return (int) $this->db->fetchOne('SELECT COUNT(*) FROM products WHERE feed_id = ?', [$feedId]);
    }
}

### 🧷 Step 2: Bind the Service in Your Package Controller

In `controller.php`:

public function on_start()
{
    $this->app->bind(
        ConcretePackageShoppingFeedManagerServiceFeedService::class,
        function ($app) {
            return new ConcretePackageShoppingFeedManagerServiceFeedService(
                $app->make(ConcreteCoreDatabaseConnectionConnection::class)
            );
        }
    );
}

### ✅ Step 3: Use It Anywhere in Concrete CMS

In a controller, task, or view:

$feedService = Core::make(ConcretePackageShoppingFeedManagerServiceFeedService::class);
$count = $feedService->countProductsInFeed($feedId);

Or with dependency injection if you’re in a controller/command.

## ✳️ Option 2: Global Helper Function (Alternative)

If you really want a global-style function (less recommended), do:

### 1. Add `application/bootstrap/app.php`:

function countProductsInFeed($feedId)
{
    $db = Database::connection();
    return (int) $db->fetchOne('SELECT COUNT(*) FROM products WHERE feed_id = ?', [$feedId]);
}

But again: **Concrete CMS favors service classes**, especially for packages and testability.

Would you like to display the product count on the dashboard list of feeds too? I can help you modify that view and controller if needed.

User
Attempted to load class "Core" from namespace "ConcreteCoreSupportFacade".
Did you forget a "use" statement for "DefuseCryptoCore"?
Assistant
Ah, good catch — that error suggests a confusion between class names.

In Concrete CMS, if you're trying to use a facade like `Core::make()`, **you need to import the correct class**, or better yet, use the recommended modern approach.

### ✅ Correct Way to Access Services in Concrete CMS

Instead of this (incorrect):

$feedService = Core::make(...);

Use **Concrete CMS's Application facade** or direct injection.

### ✅ Option 1: Use the Application Facade (Most direct)

use ConcreteCoreSupportFacadeApplication;
use ConcretePackageShoppingFeedManagerServiceFeedService;

$app = Application::getFacadeApplication();
$feedService = $app->make(FeedService::class);
$count = $feedService->countProductsInFeed($feedId);

### ✅ Option 2: Inject via Constructor (Recommended for Controllers/Tasks)

If you're in a custom controller or task class:

protected $feedService;

public function __construct(FeedService $feedService)
{
    $this->feedService = $feedService;
}

Then use:

$count = $this->feedService->countProductsInFeed($feedId);

### ⚠️ What Not to Do

Avoid `Core::make()` — it **does not exist**. It’s a **common mistake** due to older versions or confusion with `IlluminateSupportFacades`.

Let me know where exactly you want to call `countProductsInFeed()` (e.g., in a block, dashboard list, task), and I’ll tailor the code for that location.

User
Assistant
Great — you’re working in a **Concrete CMS Dashboard Controller**, and you want to **access the product count per feed** using a global method or a service method.

Since you're inside a controller extending `DashboardPageController`, the clean and proper way to access shared logic is to use a **service class** (like `FeedService`) and call its method inside your `view()` method.

### ✅ Step-by-Step: Count Products per Feed in Your Dashboard View

#### 🧱 1. First, Ensure You Have the Service Class

If not already created, make this:

**`packages/shopping_feed_manager/src/Service/FeedService.php`**

<?php
namespace ConcretePackageShoppingFeedManagerService;

use ConcreteCoreDatabaseConnectionConnection;

class FeedService
{
    protected $db;

    public function __construct(Connection $db)
    {
        $this->db = $db;
    }

    public function countProductsInFeed($feedId): int
    {
        return (int)$this->db->fetchOne("SELECT COUNT(*) FROM products WHERE feed_id = ?", [$feedId]);
    }
}

#### 🧷 2. Bind the Service in `controller.php` of the Package

In your package's `controller.php`:

public function on_start()
{
    $this->app->bind(
        ConcretePackageShoppingFeedManagerServiceFeedService::class,
        function ($app) {
            return new ConcretePackageShoppingFeedManagerServiceFeedService(
                $app->make(ConcreteCoreDatabaseConnectionConnection::class)
            );
        }
    );
}

#### 🧩 3. Modify Your Controller to Use `FeedService`

In `ShoppingFeed.php`, update your `view()` method:

use ConcretePackageShoppingFeedManagerServiceFeedService;

public function view()
{
    $db = $this->app->make(Connection::class);
    $feedService = $this->app->make(FeedService::class);

    $request = $this->request;
    $perPage = $request->query->get('limit', 10);
    $page = max(1, (int)$request->query->get('page', 1));
    $limit = $perPage;
    $offset = ($page - 1) * $limit;

    $total = (int)$db->fetchColumn("SELECT COUNT(*) FROM shoppingfeedconfiguration");
    $feeds = $db->fetchAll("SELECT * FROM shoppingfeedconfiguration ORDER BY id DESC LIMIT $offset, $limit");

    // Add product count per feed
    foreach ($feeds as &$feed) {
        $feed['product_count'] = $feedService->countProductsInFeed($feed['id']);
    }

    $this->set('feeds', $feeds);
    $this->set('total', $total);
    $this->set('page', $page);
    $this->set('limit', $limit);
}

#### 🖥️ 4. Use in Your View File

In your view file (e.g., `view.php` for the dashboard page):

<?php foreach ($feeds as $feed): ?>
    <tr>
        <td><?= h($feed['title']) ?></td>
        <td><?= h($feed['product_count']) ?> products</td>
        <td><a href="<?= Url::to('/dashboard/shopping_feed/delete_feed', $feed['id']) ?>">Delete</a></td>
    </tr>
<?php endforeach; ?>

### ✅ Result

Each feed listed in the dashboard will now show its associated **product count**, and you're using a clean, reusable **service-based structure** as per Concrete CMS best practices.

Would you like help also showing these product counts in the "Track Log" or exporting them?

User
Assistant
To **call your `FeedService` to count products per feed** inside your `ShoppingFeed` controller’s `view()` method, follow these steps — using the dependency container correctly.

Here's your updated controller code with the service **injected and used properly**:

### ✅ Step-by-Step Update of Your `view()` Method

<?php
namespace ConcretePackageShoppingFeedManagerControllerSinglePageDashboard;

use ConcreteCorePageControllerDashboardPageController;
use ConcreteCoreHttpResponseFactoryInterface;
use ConcreteCoreSupportFacadeUrl;
use ConcreteCoreDatabaseConnectionConnection;
use ConcretePackageShoppingFeedManagerServiceFeedService;

class ShoppingFeed extends DashboardPageController
{
    public function view()
    {
        $app = app(); // Concrete CMS application container
        $db = $app->make(Connection::class);
        $feedService = $app->make(FeedService::class); // Call your service

        $request = $this->request;
        $perPage = $request->query->get('limit', 10);
        $page = max(1, (int)$request->query->get('page', 1));
        $limit = $perPage;
        $offset = ($page - 1) * $limit;

        $total = (int) $db->fetchColumn("SELECT COUNT(*) FROM shoppingfeedconfiguration");
        $feeds = $db->fetchAll("SELECT * FROM shoppingfeedconfiguration ORDER BY id DESC LIMIT $offset, $limit");

        // Call the global service to attach product counts
        foreach ($feeds as &$feed) {
            $feed['product_count'] = $feedService->countProductsInFeed($feed['id']);
        }

        $this->set('feeds', $feeds);
        $this->set('total', $total);
        $this->set('page', $page);
        $this->set('limit', $limit);
    }

    public function tracklog()
    {
        $db = $this->app->make(Connection::class);
        $logs = $db->fetchAll("SELECT * FROM shoppingfeedlog");
        $this->set('logs', $logs);
    }

    public function deleteFeed($id)
    {
        $db = $this->app->make(Connection::class);
        try {
            $db->executeQuery("DELETE FROM shoppingfeedconfiguration WHERE id = ?", [$id]);
            return $this->app->make(ResponseFactoryInterface::class)
                ->json(['success' => true, 'message' => 'Feed deleted']);
        } catch (Exception $e) {
            return $this->app->make(ResponseFactoryInterface::class)
            ->json(['success' => false, 'message' => 'Invalid ID'], 400);
        }
    }
}

### 📌 Requirements Recap

– ✅ You **already registered `FeedService`** in your package’s `controller.php` using `$this->app->bind(…)`.
– ✅ The service file exists in: `packages/shopping_feed_manager/src/Service/FeedService.php`
– ✅ The `products` table has a `feed_id` field.

### ✅ Sample View Output (view.php)

In your Dashboard view file (example snippet):

<?php foreach ($feeds as $feed): ?>
    <tr>
        <td><?= h($feed['title']) ?></td>
        <td><?= h($feed['product_count']) ?> products</td>
        <td><a href="<?= Url::to('/dashboard/shopping_feed/delete_feed', $feed['id']) ?>">Delete</a></td>
    </tr>
<?php endforeach; ?>

Let me know if you want this product count to also appear as a column in a paginated table, sortable, or exportable via CSV/XML.

User
Attempted to load class "FeedService" from namespace "ConcretePackageShoppingFeedManagerService".
Did you forget a "use" statement for "ConcreteCoreFeedFeedService"?
Assistant
Thanks for the full context. You're seeing this error:

> Attempted to load class "FeedService" from namespace "ConcretePackageShoppingFeedManagerService". Did you forget a "use" statement for "ConcreteCoreFeedFeedService"?

### ✅ Diagnosis

The issue is **likely caused by autoloading not recognizing your `FeedService` class** because Concrete CMS doesn’t autoload everything in `src/` by default unless you **tell Composer** about it.

## ✅ Solution

You need to **register your `src/` folder for PSR-4 autoloading** in your `composer.json`.

### 🔧 Step 1: Update Your `composer.json`

In the **root** of your Concrete CMS project (where `composer.json` exists), update the `autoload` section like this:

"autoload": {
  "psr-4": {
    "Concrete\Package\ShoppingFeedManager\": "packages/shopping_feed_manager/src/"
  }
}

> If `autoload` already exists, just add or merge this line.

### 💻 Step 2: Run Composer Dump-Autoload

After saving the updated `composer.json`, run:

composer dump-autoload

This tells Composer to recognize your `FeedService` and all other classes under `src/`.

### ✅ Step 3: Confirm Folder/File and Namespace

Ensure the file exists:

**`packages/shopping_feed_manager/src/Service/FeedService.php`**

And the top of that file contains:

<?php
namespace ConcretePackageShoppingFeedManagerService;

use ConcreteCoreDatabaseConnectionConnection;

class FeedService
{
    protected $db;

    public function __construct(Connection $db)
    {
        $this->db = $db;
    }

    public function countProductsInFeed($feedId): int
    {
        return (int) $this->db->fetchOne("SELECT COUNT(*) FROM products WHERE feed_id = ?", [$feedId]);
    }
}

### ✅ Step 4: Use the Class

Now you can safely do this in your controller:

use ConcretePackageShoppingFeedManagerServiceFeedService;

$feedService = $this->app->make(FeedService::class);

### 🧠 Summary

| Step | Description |
|——|————-|
| ✅ 1 | Add PSR-4 autoloading to `composer.json` |
| ✅ 2 | Run `composer dump-autoload` |
| ✅ 3 | Use correct `namespace` and `use` statement |
| ✅ 4 | Class is now available in your package |

Let me know if you want to auto-register multiple services, or you’d like a service provider class setup for neatness.

User
Target class [ConcretePackageShoppingFeedManagerServiceFeedService] does not exist.
Assistant
Thanks — the error:

> `Target class [ConcretePackageShoppingFeedManagerServiceFeedService] does not exist.`

means Concrete CMS still **can't find your `FeedService` class**. This usually comes down to **one of three things**:

## ✅ Double-Check This Checklist to Fix It

### ✅ 1. **Correct File Location and Name**

Ensure your file path is:

/packages/shopping_feed_manager/src/Service/FeedService.php

And the file content starts with:

<?php
namespace ConcretePackageShoppingFeedManagerService;

use ConcreteCoreDatabaseConnectionConnection;

class FeedService {
    protected $db;

    public function __construct(Connection $db) {
        $this->db = $db;
    }

    public function countProductsInFeed($feedId): int {
        return (int)$this->db->fetchOne("SELECT COUNT(*) FROM products WHERE feed_id = ?", [$feedId]);
    }
}

❗**`FeedService.php` must match exactly** (case-sensitive on Linux).

### ✅ 2. **PSR-4 Autoloading in `composer.json`**

Make sure your root `composer.json` includes this in the `autoload` section:

"autoload": {
  "psr-4": {
    "Concrete\Package\ShoppingFeedManager\": "packages/shopping_feed_manager/src/"
  }
}

Then run:

composer dump-autoload

This regenerates the autoloader so PHP knows about your new class.

### ✅ 3. **Class is Properly Bound in Package Controller**

In `controller.php`, this should be in `on_start()`:

$this->app->bind(
    ConcretePackageShoppingFeedManagerServiceFeedService::class,
    function ($app) {
        return new ConcretePackageShoppingFeedManagerServiceFeedService(
            $app->make(ConcreteCoreDatabaseConnectionConnection::class)
        );
    }
);

### ✅ 4. **Using the Class in Controller**

In your `DashboardPageController`:

use ConcretePackageShoppingFeedManagerServiceFeedService;

$feedService = $this->app->make(FeedService::class);

## 🔁 Still Not Working?

Run this sequence again just to be sure:

cd /path/to/your/concrete-project
composer dump-autoload
clear Concrete CMS cache:
./vendor/bin/concrete c5:clear-cache

## ✅ Final Tip: Test Manually

To test autoloading manually, in your root folder, create a file `test.php`:

<?php
require 'vendor/autoload.php';

use ConcretePackageShoppingFeedManagerServiceFeedService;

$feedService = new FeedService(...); // just to test loading
echo "Service loaded!";

If this throws an error, the issue is in the autoload setup. If not — the problem is cache or binding.

Let me know your folder structure or show your `FeedService.php` if you're still stuck — I’ll debug it line by line.

Leave a Comment

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