Set Cron for XML Feed
August 7, 2025 · AI Conversations
—
## ✅ 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.
This runs and can be scheduled by the ConcreteCMS standards. If a feed will be runned within this task is a setting per feed
—
### ✅ 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`)?
—
## ✅ 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.
Did you forget a "use" statement for "DefuseCryptoCore"?
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.
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?
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.
Did you forget a "use" statement for "ConcreteCoreFeedFeedService"?
> 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.
> `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.