Concrete CMS Package Setup

User
Create concrete cms 9 package
1- crud for add title and unique feed url
2- then create front end controller that will ge erate xml based on above unique url
Assistant
Here's a complete setup for a **Concrete CMS 9 package** with:

1. **Dashboard CRUD** to manage `title` and unique `feed_url`.
2. **Frontend controller** to generate XML using the `feed_url`.

### **1. Package Structure**

packages/
└── shopping_feed_manager/
    ├── controller.php
    ├── install.xml
    ├── db.xml
    ├── routes/routes.php
    ├── src/
    │   └── Controller/
    │       └── FeedXmlController.php
    ├── single_pages/
    │   └── dashboard/
    │       └── shopping_feed/
    │           ├── feeds.php
    │           ├── add.php
    │           └── edit.php

### **2. `controller.php`**

namespace ConcretePackageShoppingFeedManager;

use ConcreteCorePackagePackage;
use ConcreteCorePageSingle;
use Route;

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

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

    public function getPackageDescription()
    {
        return t("Google Merchant Feed Manager");
    }

    public function getPackageName()
    {
        return t("Shopping Feed Manager");
    }

    public function install()
    {
        $pkg = parent::install();
        ConcreteCoreSupportFacadeDatabase::get()->Execute('
            CREATE TABLE IF NOT EXISTS feedurl (
                id INT AUTO_INCREMENT PRIMARY KEY,
                title VARCHAR(255) NOT NULL,
                url VARCHAR(255) NOT NULL UNIQUE
            )
        ');

        Single::add('/dashboard/shopping_feed/feeds', $pkg);
        Single::add('/dashboard/shopping_feed/add', $pkg);
        Single::add('/dashboard/shopping_feed/edit', $pkg);
    }

    public function on_start()
    {
        require __DIR__ . '/routes/routes.php';
    }
}

### **3. `routes/routes.php`**

use ConcreteCoreRoutingRouter;

$router = Router::getInstance();
$router->get('/shopping-feed/{url}', 'ConcretePackageShoppingFeedManagerControllerFeedXmlController::generate');

### **4. `src/Controller/FeedXmlController.php`**

namespace ConcretePackageShoppingFeedManagerController;

use ConcreteCoreControllerController;
use ConcreteCoreSupportFacadeApplication;
use SymfonyComponentHttpFoundationResponse;

class FeedXmlController extends Controller
{
    public function generate($url)
    {
        $db = Database::connection();
        $feed = $db->fetchAssoc('SELECT * FROM feedurl WHERE url = ?', [$url]);

        if (!$feed) {
            return new Response('Feed not found', 404);
        }

        $xml = new SimpleXMLElement('<feed/>');
        $xml->addChild('title', $feed['title']);
        $xml->addChild('url', $feed['url']);

        // You can add product listing here too...

        $response = new Response($xml->asXML());
        $response->headers->set('Content-Type', 'application/xml');
        return $response;
    }
}

### **5. `single_pages/dashboard/shopping_feed/add.php`**

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

<form method="post" action="<?= $this->action('save') ?>">
    <div class="form-group">
        <label>Title</label>
        <input class="form-control" name="title" required>
    </div>
    <div class="form-group">
        <label>Unique URL (slug)</label>
        <input class="form-control" name="url" required>
    </div>
    <button class="btn btn-primary" type="submit">Save</button>
</form>

### **6. `single_pages/dashboard/shopping_feed/feeds.php`**

<?php
defined('C5_EXECUTE') or die("Access Denied.");
$db = Database::connection();
$feeds = $db->fetchAll("SELECT * FROM feedurl");
?>

<h2>Feeds</h2>
<ul>
    <?php foreach ($feeds as $feed): ?>
        <li><?= h($feed['title']) ?> - <a href="/shopping-feed/<?= h($feed['url']) ?>" target="_blank">View XML</a></li>
    <?php endforeach; ?>
</ul>

### **7. `single_pages/dashboard/shopping_feed/edit.php`**
You can clone `add.php` and prefill values using `$_GET['id']`, then update on POST.

Let me know if you want this to include **form controllers**, **CSRF tokens**, or **validation**, or if you'd like an exportable `.zip` package.

User
How to handle form submit
Assistant
To handle form submission in your Concrete CMS 9 package (e.g., for the **Add Feed** form), you need to:

1. Create a **controller for the single page**
2. Implement a `save()` method inside that controller
3. Use `$this->action('save')` in the form to route it

### **1. Create Controller File for Add Page**
File: `packages/shopping_feed_manager/controllers/single_page/dashboard/shopping_feed/add.php`

<?php
namespace ConcretePackageShoppingFeedManagerControllerSinglePageDashboardShoppingFeed;

use ConcreteCorePageControllerDashboardPageController;
use ConcreteCoreSupportFacadeDatabase;
use ConcreteCoreRoutingRedirect;

class Add extends DashboardPageController
{
    public function save()
    {
        $title = $this->post('title');
        $url = $this->post('url');

        if (!$title || !$url) {
            $this->set('error', t("Both Title and URL are required."));
            return;
        }

        $db = Database::connection();
        $exists = $db->fetchColumn("SELECT COUNT(*) FROM feedurl WHERE url = ?", [$url]);

        if ($exists) {
            $this->set('error', t("Feed URL must be unique."));
            return;
        }

        $db->insert('feedurl', [
            'title' => $title,
            'url' => $url,
        ]);

        $this->flash('success', t("Feed saved successfully."));
        return $this->redirect('/dashboard/shopping_feed/feeds');
    }
}

### **2. Update `add.php` View File**
File: `single_pages/dashboard/shopping_feed/add.php`

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

<?php if (isset($error)) : ?>
    <div class="alert alert-danger"><?= $error ?></div>
<?php endif; ?>

<form method="post" action="<?= $this->action('save') ?>">
    <?= $this->controller->token->output('save') ?>

    <div class="form-group">
        <label>Title</label>
        <input class="form-control" name="title" required>
    </div>

    <div class="form-group">
        <label>Unique URL (slug)</label>
        <input class="form-control" name="url" required>
    </div>

    <button class="btn btn-primary" type="submit">Save</button>
</form>

### **3. Explanation**
– `DashboardPageController` is used to manage dashboard logic
– `$this->action('save')` builds the form submission route
– `$this->post('title')` gets POST data
– `$this->flash()` shows messages after redirect
– `token->output('save')` protects against CSRF

Would you like to add edit/delete functionality or AJAX validation next?

User
Also add a Settings in add.php
In which we will set price range, product category.
And then Generate google merchant xml based on feed item settings
Also add delete Ajax and edit
Assistant
User is building a Concrete CMS 9 package with CRUD functionality for managing Google Merchant feeds, including title, unique feed URL, settings like price range and product category, and frontend XML generation. They also want to implement edit, delete (with AJAX), and generate XML based on stored settings.
Assistant
Great! Here’s how to fully enhance your Concrete CMS 9 package:

### **1. Updated DB Table Schema**
Update your `install()` method or `db.xml` to include the new fields:

ALTER TABLE feedurl 
ADD COLUMN settings TEXT DEFAULT NULL;

Or update the `CREATE TABLE` in `controller.php`:

CREATE TABLE IF NOT EXISTS feedurl (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    url VARCHAR(255) NOT NULL UNIQUE,
    settings TEXT DEFAULT NULL
)

### **2. Updated Form in `add.php` View**
Add `price_range` and `product_category` fields inside your form:

<div class="form-group">
    <label>Price Range</label>
    <input class="form-control" name="price_range" placeholder="e.g. 100-500">
</div>

<div class="form-group">
    <label>Product Category</label>
    <input class="form-control" name="product_category" placeholder="e.g. Furniture">
</div>

### **3. Updated `save()` Method in Controller**
Inside `Add.php`:

$settings = json_encode([
    'price_range' => $this->post('price_range'),
    'product_category' => $this->post('product_category'),
]);

$db->insert('feedurl', [
    'title' => $title,
    'url' => $url,
    'settings' => $settings,
]);

### **4. Frontend XML Based on Settings**
In `FeedXmlController.php`:

$settings = json_decode($feed['settings'], true);

$xml->addChild('title', $feed['title']);
$xml->addChild('category', $settings['product_category']);
$xml->addChild('price_range', $settings['price_range']);

// Add products matching these settings...

You can fetch matching products using a query like:

$range = explode('-', $settings['price_range']);
$min = (float) $range[0];
$max = (float) $range[1];

$products = $db->fetchAll("SELECT * FROM products WHERE category = ? AND price BETWEEN ? AND ?", [
    $settings['product_category'], $min, $max
]);

### **5. Add AJAX Delete in `feeds.php`**
Add a delete button and JS:

#### Button:

<button class="btn btn-danger btn-sm delete-feed" data-id="<?= $feed['id'] ?>">Delete</button>

#### JavaScript:

<script>
document.querySelectorAll('.delete-feed').forEach(btn => {
    btn.addEventListener('click', function () {
        const id = this.dataset.id;
        if (confirm("Are you sure you want to delete this feed?")) {
            fetch('<?= $view->action("delete_feed") ?>', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-Token': '<?= $controller->token->generate("delete_feed") ?>'
                },
                body: JSON.stringify({ id })
            })
            .then(res => res.json())
            .then(data => {
                if (data.success) location.reload();
                else alert(data.message);
            });
        }
    });
</script>

### **6. Add `delete_feed()` in Feeds Controller**
File: `controllers/single_page/dashboard/shopping_feed/feeds.php`

public function delete_feed()
{
    $this->token->validate('delete_feed');

    $json = json_decode(file_get_contents('php://input'), true);
    $id = $json['id'];

    $db = Database::connection();
    $db->delete('feedurl', ['id' => $id]);

    return new ConcreteCoreHttpJsonResponse(['success' => true]);
}

### **7. Edit Page Setup**
In `edit.php`, fetch the feed by ID, prefill fields, and use `$this->action('update', $feed['id'])`.

Then in `Edit.php` controller, create `update($id)` method similar to `save()`.

Let me know if you'd like the zip-ready package or want the edit form and controller done in full too.

Leave a Comment

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