Manage add/edit form

User
manage add/edit form same controller and open same page like feed.php in concrete cms
Assistant
Got it. Here’s a clean pattern for **one Single Page (feed.php)** that handles both **Add** and **Edit** in the same controller in Concrete CMS 9+.

# 1) Single Page + Controller structure

/application
  /single_pages
    feed.php                <-- one page for both add & edit
  /controllers/single_page
    feed.php                <-- controller

> If this lives in a package, mirror the same structure under `/packages/your_pkg_handle/…`.

# 2) Controller (application/controllers/single_page/feed.php)

<?php
namespace ApplicationControllerSinglePage;

use ConcreteCorePageControllerPageController;
use ConcreteCoreHttpResponseFactoryInterface;
use ConcreteCoreUrlResolverManagerResolverManagerInterface;
use ConcreteCoreValidationCSRFToken;
use DoctrineDBALConnection; // simple DB example
use ConcreteCoreSupportFacadeUrl;

class Feed extends PageController
{
    protected Connection $db;
    protected Token $token;

    public function on_start()
    {
        $this->db = $this->app->make(Connection::class);
        $this->token = $this->app->make(Token::class);
    }

    /**
     * Open same page for Add (no id) or Edit (with id).
     * /feed                   -> Add mode
     * /feed/123               -> Edit mode (id=123)
     * or /feed?id=123         -> also works
     */
    public function view($id = null)
    {
        // Accept id from path or query
        $id = $id ?: (int)($this->request->query->get('id') ?? 0);

        $feed = null;
        if ($id > 0) {
            $feed = $this->db->fetchAssociative('SELECT * FROM Feeds WHERE id = ?', [$id]);
            if (!$feed) {
                $this->flash('error', t('Feed not found.'));
                return $this->redirect(Url::to('/feed'));
            }
        }

        $this->set('feed', $feed);               // null = Add mode
        $this->set('isEdit', (bool)$feed);
        $this->set('actionURL', $this->action('save')); // form action
    }

    /**
     * Handle both create + update
     */
    public function save()
    {
        if (!$this->token->validate('feed_save')) {
            $this->flash('error', $this->token->getErrorMessage());
            return $this->buildRedirectAfterSave();
        }

        $id    = (int)$this->request->request->get('id');
        $title = trim((string)$this->request->request->get('title'));
        $url   = trim((string)$this->request->request->get('url'));

        // Basic validation
        $errors = [];
        if ($title === '') $errors[] = t('Title is required.');
        if ($url === '')   $errors[] = t('URL is required.');

        if ($errors) {
            foreach ($errors as $e) $this->flash('error', $e);
            return $this->buildRedirectAfterSave($id);
        }

        if ($id > 0) {
            // UPDATE
            $exists = $this->db->fetchOne('SELECT COUNT(*) FROM Feeds WHERE id = ?', [$id]);
            if (!$exists) {
                $this->flash('error', t('Feed not found.'));
                return $this->redirect(Url::to('/feed'));
            }
            $this->db->update('Feeds', ['title' => $title, 'url' => $url], ['id' => $id]);
            $this->flash('success', t('Feed updated.'));
            return $this->redirect(Url::to('/feed', $id)); // stay on same page in edit mode
        } else {
            // INSERT
            $this->db->insert('Feeds', ['title' => $title, 'url' => $url]);
            $newId = (int)$this->db->lastInsertId();
            $this->flash('success', t('Feed created.'));
            return $this->redirect(Url::to('/feed', $newId)); // jump into edit mode of the new record
        }
    }

    /**
     * Optional: delete from same page
     */
    public function delete($id = null)
    {
        $id = (int)$id;
        if (!$this->token->validate('feed_delete_'.$id)) {
            $this->flash('error', $this->token->getErrorMessage());
            return $this->redirect(Url::to('/feed', $id));
        }
        $this->db->delete('Feeds', ['id' => $id]);
        $this->flash('success', t('Feed deleted.'));
        return $this->redirect(Url::to('/feed'));
    }

    private function buildRedirectAfterSave(?int $id = null)
    {
        return $id ? $this->redirect(Url::to('/feed', $id)) : $this->redirect(Url::to('/feed'));
    }
}

**Notes**
– `view($id = null)` receives **path args**, so `/feed/42` passes `42` to `view`.
– You can also support `?id=42` for convenience.
– One `save()` handles both create/update by checking if `id` exists.
– Uses **DBAL** for brevity (`Feeds` table with `id`, `title`, `url`). Swap for Doctrine entities if you prefer.

# 3) Single Page view (application/single_pages/feed.php)

<?php
defined('C5_EXECUTE') or die('Access Denied.');
/** @var array|null $feed */
/** @var bool $isEdit */
/** @var string $actionURL */

use ConcreteCoreSupportFacadeUrl;
use ConcreteCoreValidationCSRFToken;

/** @var Token $token */
$token = app(Token::class);

$id    = $feed['id']   ?? 0;
$title = $feed['title']?? '';
$url   = $feed['url']  ?? '';
?>

<div class="ccm-ui">
  <h2><?= $isEdit ? t('Edit Feed') : t('Add Feed'); ?></h2>

  <form method="post" action="<?= h($actionURL) ?>">
    <?php $token->output('feed_save'); ?>
    <input type="hidden" name="id" value="<?= (int)$id ?>">

    <div class="form-group">
      <label for="title"><?= t('Title') ?></label>
      <input type="text" class="form-control" id="title" name="title" value="<?= h($title) ?>" required>
    </div>

    <div class="form-group">
      <label for="url"><?= t('URL') ?></label>
      <input type="url" class="form-control" id="url" name="url" value="<?= h($url) ?>" required>
    </div>

    <div class="mt-3 d-flex gap-2">
      <button type="submit" class="btn btn-primary">
        <?= $isEdit ? t('Update') : t('Create') ?>
      </button>

      <?php if ($isEdit) { ?>
        <form method="post" action="<?= Url::to('/feed', 'delete', $id) ?>" class="d-inline">
          <?php app(Token::class)->output('feed_delete_'.$id); ?>
          <button type="submit" class="btn btn-danger" onclick="return confirm('<?= t('Delete this feed?') ?>')">
            <?= t('Delete') ?>
          </button>
        </form>
      <?php } ?>
    </div>
  </form>

  <hr>

  <h3 class="mt-4"><?= t('All Feeds') ?></h3>
  <table class="table table-striped">
    <thead>
      <tr>
        <th><?= t('ID') ?></th>
        <th><?= t('Title') ?></th>
        <th><?= t('URL') ?></th>
        <th><?= t('Actions') ?></th>
      </tr>
    </thead>
    <tbody>
      <?php
      $db = app(DoctrineDBALConnection::class);
      $rows = $db->fetchAllAssociative('SELECT * FROM Feeds ORDER BY id DESC');
      foreach ($rows as $row) { ?>
        <tr>
          <td><?= (int)$row['id'] ?></td>
          <td><?= h($row['title']) ?></td>
          <td><a href="<?= h($row['url']) ?>" target="_blank"><?= h($row['url']) ?></a></td>
          <td>
            <a class="btn btn-sm btn-secondary" href="<?= Url::to('/feed', (int)$row['id']) ?>">
              <?= t('Edit on same page') ?>
            </a>
          </td>
        </tr>
      <?php } ?>
    </tbody>
  </table>
</div>

**How it works**
– **Add Mode**: open `/feed` → empty form.
– **Edit Mode**: open `/feed/123` (or `/feed?id=123`) → same page, fields prefilled.
– The “All Feeds” table links to **the same page** with the id, so you stay on feed.php.

# 4) Register the Single Page (once)

Programmatically (in your package controller `install()` / `upgrade()`), or via Dashboard > Pages > Single Pages:

use ConcreteCorePageSingle as SinglePage;

public function install()
{
    parent::install();
    $sp = SinglePage::add('/feed', $this->getPackage());
    if ($sp) {
        $sp->update(['cName' => t('Feed Manager')]);
    }
}

# 5) Optional: simple table

For quick testing:

CREATE TABLE Feeds (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(255) NOT NULL,
  url VARCHAR(1024) NOT NULL
);

## TL;DR
– Use **one Single Page** (`/feed`) and pass an **optional ID** to `view($id)`.
– Populate the same form for **add** (no id) or **edit** (with id).
– Handle **save()** for both create/update; redirect back to `/feed/{id}` to remain on the same page.

If you want the Doctrine Entity version instead of raw DBAL, say the word and I’ll swap the sample to entities + repository.

User
also feed/add and feed/edit/2
Assistant
Totally—let’s support clean URLs like:

– `/feed/add` → add mode (empty form)
– `/feed/edit/2` → edit feed with ID 2
– (you can still keep `/feed` and `/feed/2` working too)

You’ve got two good ways. Here’s the cleanest:

# Option A — Pretty routes mapped to the same Single Page controller (recommended)

## 1) Register routes (package `on_start()` or `RouteList`)

use ConcreteCoreRoutingRouter;

public function on_start()
{
    /** @var Router $router */
    $router = $this->app->make(Router::class);

    // views
    $router->get('/feed/add',  'ApplicationControllerSinglePageFeed::add');
    $router->get('/feed/edit/{id}', 'ApplicationControllerSinglePageFeed::edit')
           ->setRequirements(['id' => 'd+']);

    // actions (post)
    $router->post('/feed/save', 'ApplicationControllerSinglePageFeed::save');
    $router->post('/feed/delete/{id}', 'ApplicationControllerSinglePageFeed::delete')
           ->setRequirements(['id' => 'd+']);
}

> If you’re not in a package, put the same in `/application/bootstrap/app.php` after getting `$router`.

## 2) Controller (`application/controllers/single_page/feed.php`)
Add two tiny methods that reuse your existing logic:

public function add()
{
    // Reuse the same view with "no id" = add mode
    return $this->view(null);
}

public function edit($id = null)
{
    // Reuse the same view with an id = edit mode
    return $this->view((int)$id);
}

Your existing `save()` and `delete()` stay the same.
(They’re now hit via `/feed/save` and `/feed/delete/{id}`.)

## 3) View links (`application/single_pages/feed.php`)
Point buttons/links to the pretty routes:

use ConcreteCoreSupportFacadeUrl;

// "Create new" button:
<a class="btn btn-primary" href="<?= Url::to('/feed/add') ?>"><?= t('Add New Feed') ?></a>

// Each row's edit button:
<a class="btn btn-sm btn-secondary" href="<?= Url::to('/feed/edit', (int)$row['id']) ?>">
  <?= t('Edit') ?>
</a>

// Form action (create/update):
<form method="post" action="<?= h(Url::to('/feed/save')) ?>">

// Delete (per row):
<form method="post" action="<?= Url::to('/feed/delete', (int)$row['id']) ?>" class="d-inline">
    <?php app(ConcreteCoreValidationCSRFToken::class)->output('feed_delete_'.(int)$row['id']); ?>
    <button type="submit" class="btn btn-danger" onclick="return confirm('<?= t('Delete this feed?') ?>')">
        <?= t('Delete') ?>
    </button>
</form>

That’s it—now `/feed/add` and `/feed/edit/2` render the **same page** (`feed.php`) but in the right mode.

# Option B — Use built-in Single Page actions (quick, but URL shows `/-/`)
If you don’t want custom routes, just use:
– `$this->action('add')` → `/feed/-/add`
– `$this->action('edit', $id)` → `/feed/-/edit/2`

And add `add()` / `edit($id)` methods like above. Works fine, just not as “pretty”.

If you want both `/feed/2` and `/feed/edit/2` to work, keep your original `view($id)` path behavior and also register the `/feed/edit/{id}` route—both will call the same code.

User
always want to call Feed::view not add or edit
Assistant
Perfect—we can keep **only `Feed::view()`** and still support pretty URLs:

– `/feed` → add mode
– `/feed/12` → edit 12
– `/feed/add` → add mode (calls `view()`)
– `/feed/edit/12` → edit 12 (calls `view(12)`)

### 1) Routes (map to `view` only)
Put in your package `on_start()` (or `/application/bootstrap/app.php`):

use ConcreteCoreRoutingRouter;

public function on_start()
{
    $router = $this->app->make(Router::class);

    // Pretty routes that ALL hit Feed::view
    $router->get('/feed/add', 'ApplicationControllerSinglePageFeed::view');
    $router->get('/feed/edit/{id}', 'ApplicationControllerSinglePageFeed::view')
           ->setRequirements(['id' => 'd+']);

    // Post actions (unchanged)
    $router->post('/feed/save', 'ApplicationControllerSinglePageFeed::save');
    $router->post('/feed/delete/{id}', 'ApplicationControllerSinglePageFeed::delete')
           ->setRequirements(['id' => 'd+']);
}

### 2) Controller: detect mode inside `view()`
Update your `view()` to infer **add/edit** by URL path, but still use one method:

public function view($id = null)
{
    $path = rtrim((string)$this->request->getPath(), '/');

    $isAddPath  = (bool)preg_match('#/feed/add$#', $path);
    $isEditPath = (bool)preg_match('#/feed/edit/d+$#', $path);

    // Accept id from path (/feed/12), pretty edit (/feed/edit/12), or ?id=12
    $id = $id ?: (int)($this->request->query->get('id') ?? 0);

    $mode = 'add';
    if ($isAddPath) {
        $mode = 'add';
    } elseif ($isEditPath || $id > 0) {
        $mode = 'edit';
    }

    $feed = null;
    if ($mode === 'edit') {
        if ($id <= 0) {
            $this->flash('error', t('Missing feed ID.'));
            return $this->redirect(ConcreteCoreSupportFacadeUrl::to('/feed'));
        }
        $feed = $this->db->fetchAssociative('SELECT * FROM Feeds WHERE id = ?', [$id]);
        if (!$feed) {
            $this->flash('error', t('Feed not found.'));
            return $this->redirect(ConcreteCoreSupportFacadeUrl::to('/feed'));
        }
    }

    $this->set('feed', $feed);
    $this->set('mode', $mode);                      // 'add' or 'edit'
    $this->set('actionURL', $this->app->make('url')->to('/feed/save'));
}

### 3) Save redirects (stay with pretty routes; still hits `view()`)

if ($id > 0) {
    // after update
    return $this->redirect(ConcreteCoreSupportFacadeUrl::to('/feed/edit', $id));
} else {
    // after insert
    return $this->redirect(ConcreteCoreSupportFacadeUrl::to('/feed/edit', $newId));
}

### 4) View links

// Add new
<a class="btn btn-primary" href="<?= ConcreteCoreSupportFacadeUrl::to('/feed/add') ?>">
  <?= t('Add New Feed') ?>
</a>

<!-- Row edit -->
<a class="btn btn-sm btn-secondary" href="<?= ConcreteCoreSupportFacadeUrl::to('/feed/edit', (int)$row['id']) ?>">
  <?= t('Edit') ?>
</a>

<!-- Form action -->
<form method="post" action="<?= h(ConcreteCoreSupportFacadeUrl::to('/feed/save')) ?>">

That’s it—**no `add()` or `edit()` methods at all**. All routes land in `view()`, which decides the mode based on the path/id.

User
Page Not Found
Assistant
“Page Not Found” in Concrete usually means one of three things:
1) the Single Page isn’t installed, 2) the controller/namespace/path doesn’t match, or 3) your custom routes aren’t actually registered (or URL rewriting is off).

Here’s a tight checklist to make **/feed**, **/feed/add**, and **/feed/edit/2** all hit **Feed::view()**.

# 1) Single Page exists
– Make sure the page is installed once at **/feed**.
– If in a package (recommended):

use ConcreteCorePageSingle as SinglePage;

    public function install()
    {
        $pkg = parent::install();
        if (!ConcreteCoreSupportFacadePage::getByPath('/feed')->getCollectionID()) {
            $sp = SinglePage::add('/feed', $pkg);
            if ($sp) { $sp->update(['cName' => t('Feed Manager')]); }
        }
    }

– Or via Dashboard → Pages & Themes → Single Pages → “Add Single Page” → enter `/feed`.

– File locations (application version):

/application/single_pages/feed.php
  /application/controllers/single_page/feed.php

Package version:

/packages/<pkg_handle>/single_pages/feed.php
  /packages/<pkg_handle>/controllers/single_page/feed.php

# 2) Controller namespace/path match
**Application version**

<?php
namespace ApplicationControllerSinglePage;

use ConcreteCorePageControllerPageController;

class Feed extends PageController
{
    public function view($id = null) { /* … */ }
    public function save() { /* … */ }
    public function delete($id = null) { /* … */ }
}

**Package version**

<?php
namespace ConcretePackage<PkgHandle>ControllerSinglePage;

use ConcreteCorePageControllerPageController;

class Feed extends PageController
{
    public function view($id = null) { /* … */ }
    // …
}

> The namespace MUST match the file location. If it doesn’t, Concrete won’t find the controller and you’ll see “Page Not Found”.

# 3) Route all pretty URLs to `view()` (not `add`/`edit`)
Register routes in **one place** only.

**Application (no package):** `/application/bootstrap/app.php`

use ConcreteCoreRoutingRouter;

$app->extend(Router::class, function (Router $router) {
    // Everything hits Feed::view
    $router->get('/feed/add', 'ApplicationControllerSinglePageFeed::view');
    $router->get('/feed/edit/{id}', 'ApplicationControllerSinglePageFeed::view')
           ->setRequirements(['id' => 'd+']);

    // Post actions:
    $router->post('/feed/save', 'ApplicationControllerSinglePageFeed::save');
    $router->post('/feed/delete/{id}', 'ApplicationControllerSinglePageFeed::delete')
           ->setRequirements(['id' => 'd+']);

    return $router;
});

**Package:** in your package controller `on_start()`

use ConcreteCoreRoutingRouter;

public function on_start()
{
    $router = $this->app->make(Router::class);

    $router->get('/feed/add', 'ConcretePackage<PkgHandle>ControllerSinglePageFeed::view');
    $router->get('/feed/edit/{id}', 'ConcretePackage<PkgHandle>ControllerSinglePageFeed::view')
           ->setRequirements(['id' => 'd+']);

    $router->post('/feed/save', 'ConcretePackage<PkgHandle>ControllerSinglePageFeed::save');
    $router->post('/feed/delete/{id}', 'ConcretePackage<PkgHandle>ControllerSinglePageFeed::delete')
           ->setRequirements(['id' => 'd+']);
}

> If your controller is under Application but you register package namespaces (or vice-versa), you’ll get 404.

# 4) Quick sanity checks
– Try direct URL with dispatcher: **/index.php/feed** and **/index.php/feed/add**
– If these work, your issue is URL rewriting → fix `.htaccess` (Pretty URLs) in web root:

RewriteEngine On
    RewriteBase /
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . index.php [L]

Enable in Dashboard → System & Settings → SEO & Statistics → Pretty URLs.
– Clear cache: Dashboard → System & Settings → Optimization → Clear Cache (or CLI `./concrete/bin/concrete5 c5:clear-cache`)
– Refresh Single Pages (find controller): Dashboard → Pages & Themes → Single Pages → “Rescan Single Pages”
or CLI:

./concrete/bin/concrete5 c5:single-pages:refresh

– List routes (to confirm yours are registered):

./concrete/bin/concrete5 c5:routing:list | grep feed

– Controller method exists? If `Feed::view` fatals, Concrete may return 404—check logs: `/application/files/logs/` or Dashboard → Reports → Logs.

# 5) `view()` mode detection (one method)
Inside your controller, keep it simple:

public function view($id = null)
{
    $path = rtrim((string)$this->request->getPath(), '/');
    $isAdd  = (bool)preg_match('#/feed/add$#', $path);
    $isEdit = (bool)preg_match('#/feed/edit/d+$#', $path);

    $id = $id ?: (int)($this->request->query->get('id') ?? 0);
    $mode = ($isAdd) ? 'add' : (($isEdit || $id > 0) ? 'edit' : 'add');

    // fetch record if editing…
    // $this->set('mode', $mode); etc.
}

# 6) Common pitfalls that cause 404
– Single Page not installed at `/feed`.
– Wrong controller namespace for location (Application vs Package).
– Routes registered in the wrong file or not executed (e.g., `on_start()` never called because package not installed/enabled).
– Cache not cleared after adding controller/routes.
– Pretty URLs off or broken `.htaccess`.
– Case sensitivity on Linux paths (`feed.php` vs `Feed.php`).

If you paste your **controller namespace** line and **where you registered the routes**, I’ll pinpoint the exact mismatch.

User
protected function addDashboardPages()
{
$pages = [
'/dashboard/shopping_feed' => [t('Shopping Feed'), t('Shopping Feed Tools')],
'/dashboard/shopping_feed/view' => [t('Manage Feeds'), t('List and manage feeds'), true],
//'/dashboard/shopping_feed/tracklog' => [t('Tracking Log'), t('View tracking log')],
// '/dashboard/shopping_feed/add' => [t('Add Feed'), t('Add a new feed')],
// '/dashboard/shopping_feed/edit' => [t('Edit Feed'), t('Edit an existing feed')],

'/dashboard/shopping_feed/feed' => [t('Feed'), t('Feed')],
'/dashboard/shopping_feed/settings' => [t('Settings'), t('Settings')],
];

foreach ($pages as $path => $pageData) {
list($name, $desc) = $pageData;
$redirect = $pageData[2] ?? false;

$page = Page::getByPath($path);
if ($page->isError()) {
$page = Single::add($path, $this);
}
if (is_object($page) && !$page->isError()) {
$page->update(['cName' => $name, 'cDescription' => $desc]);
if($path == '/dashboard/shopping_feed/add' || $path == '/dashboard/shopping_feed/edit' || $path == '/dashboard/shopping_feed/feed') {
$page->setAttribute('exclude_nav', true);
}
}
}

// Add alias under Community Store Settings, only if it exists
$pkgService = Core::make(PackageService::class);
$communityStore = $pkgService->getByHandle('community_store');

if ($communityStore && is_object($communityStore)) {
$mainPage = Page::getByPath('/dashboard/shopping_feed');
$parent = Page::getByPath('/dashboard/store');
$aliasExists = Page::getByPath('/dashboard/store/feed');

$isMainPageValid = ($mainPage instanceof Page && !$mainPage->isError());
$isParentValid = ($parent instanceof Page && !$parent->isError());
$aliasAlreadyExists = ($aliasExists instanceof Page && !$aliasExists->isError());

if ($isMainPageValid && $isParentValid && !$aliasAlreadyExists) {
$alias = $mainPage->addCollectionAlias($parent);

if ($alias instanceof Page && !$alias->isError()) {
$alias->update(['cName' => t('Feed')]);
}
}
}
}
we adding like this and all other pages working fine

Assistant
Great — your page installer looks fine. Since you want **all URLs to hit `Feed::view()`** (no `add`/`edit` actions), here’s exactly what to add for the **Dashboard** single page:

## 1) Make sure the files/namespace match the Dashboard path

**Files (inside your package):**

/packages/<your_handle>/single_pages/dashboard/shopping_feed/feed.php
/packages/<your_handle>/controllers/single_page/dashboard/shopping_feed/feed.php

**Controller namespace & class:**

<?php
namespace ConcretePackage<YourHandle>ControllerSinglePageDashboardShoppingFeed;

use ConcreteCorePageControllerDashboardPageController;
use ConcreteCoreSupportFacadeUrl;

class Feed extends DashboardPageController
{
    public function view($id = null)
    {
        // Detect /add or /edit/{id} while still using ONE method
        $path = rtrim((string)$this->request->getPath(), '/');
        $isAdd  = (bool)preg_match('#/dashboard/shopping_feed/feed/add$#', $path);
        $isEdit = (bool)preg_match('#/dashboard/shopping_feed/feed/edit/d+$#', $path);

        // Accept id from /feed/{id} (path arg) or /feed/edit/{id} or ?id=
        $id = $id ?: (int)($this->request->query->get('id') ?? 0);

        $mode = $isAdd ? 'add' : (($isEdit || $id > 0) ? 'edit' : 'add');

        // Load record only in edit mode…
        // $feed = ...
        // $this->set('feed', $feed);
        $this->set('mode', $mode);
        $this->set('actionURL', Url::to('/dashboard/shopping_feed/feed', 'save')); // POST action (see 3)
    }

    public function save()
    {
        // handle create/update, then redirect back nicely:
        // return $this->redirect(Url::to('/dashboard/shopping_feed/feed/edit', $newId));
    }

    public function delete($id = null)
    {
        // delete then redirect:
        // return $this->redirect(Url::to('/dashboard/shopping_feed/feed'));
    }
}

> If you put the controller under `/application/…`, change the namespace to `ApplicationControllerSinglePageDashboardShoppingFeed`.

## 2) Register pretty routes that all call `Feed::view()`

Do this in your **package controller** `on_start()`:

use ConcreteCoreRoutingRouter;

public function on_start()
{
    $router = $this->app->make(Router::class);

    // Every GET goes to Feed::view (one method)
    $router->get('/dashboard/shopping_feed/feed', 'ConcretePackage<YourHandle>ControllerSinglePageDashboardShoppingFeedFeed::view');
    $router->get('/dashboard/shopping_feed/feed/add', 'ConcretePackage<YourHandle>ControllerSinglePageDashboardShoppingFeedFeed::view');
    $router->get('/dashboard/shopping_feed/feed/edit/{id}', 'ConcretePackage<YourHandle>ControllerSinglePageDashboardShoppingFeedFeed::view')
           ->setRequirements(['id' => 'd+']);

    // POST actions (normal actions on the same controller)
    $router->post('/dashboard/shopping_feed/feed/save', 'ConcretePackage<YourHandle>ControllerSinglePageDashboardShoppingFeedFeed::save');
    $router->post('/dashboard/shopping_feed/feed/delete/{id}', 'ConcretePackage<YourHandle>ControllerSinglePageDashboardShoppingFeedFeed::delete')
           ->setRequirements(['id' => 'd+']);
}

> If your controller is under `Application…`, update the FQCNs accordingly.

## 3) View file: point links/actions at the pretty routes

In `/single_pages/dashboard/shopping_feed/feed.php`:

<?php
use ConcreteCoreSupportFacadeUrl;
/** @var string $actionURL */
/** @var string $mode */ ?>

<a class="btn btn-primary" href="<?= Url::to('/dashboard/shopping_feed/feed/add') ?>">
  <?= t('Add New Feed') ?>
</a>

<!-- Edit buttons in your list -->
<a class="btn btn-sm btn-secondary" href="<?= Url::to('/dashboard/shopping_feed/feed/edit', (int)$row['id']) ?>">
  <?= t('Edit') ?>
</a>

<!-- Form submit (create/update) -->
<form method="post" action="<?= h(Url::to('/dashboard/shopping_feed/feed/save')) ?>">
  <!-- fields… -->
</form>

<!-- Delete -->
<form method="post" action="<?= Url::to('/dashboard/shopping_feed/feed/delete', (int)$row['id']) ?>" class="d-inline">
  <!-- CSRF + submit… -->
</form>

## 4) Your installer (`addDashboardPages`) is fine
You already add **only** `/dashboard/shopping_feed/feed` as a single page — that’s correct.
You **don’t** add `/add` or `/edit` as pages; they’re just routes that map to the same controller method.

## 5) If you still see “Page Not Found”
Quick checks:

– The single page exists: `/dashboard/shopping_feed/feed` opens.
– Controller namespace matches its file location exactly (Package vs Application).
– Routes are actually registered (run: `./concrete/bin/concrete5 c5:routing:list | grep shopping_feed`).
– Clear cache: Dashboard → System & Settings → Optimization → Clear Cache (or CLI).
– Try `…/index.php/dashboard/shopping_feed/feed/add`. If that works, fix Pretty URLs (`.htaccess`) though dashboard usually doesn’t need special rules beyond the site’s.

This setup gives you:

– `/dashboard/shopping_feed/feed` → add mode
– `/dashboard/shopping_feed/feed/add` → add mode (same `view()`)
– `/dashboard/shopping_feed/feed/edit/2` → edit ID 2 (same `view()`)

…all handled by **one** method: `Feed::view()`.

User
still not work
Assistant
Gotcha—let’s get you unblocked fast.

## Quickest fix (works even if routes aren’t registering)
Since Concrete’s dispatcher treats `/dashboard/…/feed/edit/2` as **an action** called `edit`, it 404s because that method doesn’t exist. Add two tiny **proxy methods** in your controller that *just call* `view()`:

**`/packages//controllers/single_page/dashboard/shopping_feed/feed.php`**

<?php
namespace ConcretePackage<YourHandle>ControllerSinglePageDashboardShoppingFeed;

use ConcreteCorePageControllerDashboardPageController;

class Feed extends DashboardPageController
{
    public function view($id = null)
    {
        // your existing view logic
        // infer $mode = add/edit and load $feed if $id > 0...
    }

    // PROXY: /dashboard/shopping_feed/feed/add  → same view (add mode)
    public function add()
    {
        return $this->view(null);
    }

    // PROXY: /dashboard/shopping_feed/feed/edit/2 → same view with id
    public function edit($id = null)
    {
        return $this->view((int)$id);
    }
}

No routing config needed. This immediately makes:
– `/dashboard/shopping_feed/feed/add` → **view()** (add mode)
– `/dashboard/shopping_feed/feed/edit/2` → **view(2)** (edit mode)

> You’re still rendering only the single `feed.php` view. These methods are just tiny pass-throughs so the dispatcher doesn’t 404.

Now **clear cache** (Dashboard → System & Settings → Optimization → Clear Cache) and try again.

## If you still prefer pretty routes that *map to view()*
If you *must* keep only `view()` and want `/edit/{id}` to call `view` directly via the router, double-check these **three exact points**:

1) **Controller FQCN matches location**
File path:

/packages/<your_handle>/controllers/single_page/dashboard/shopping_feed/feed.php

Namespace/class:

namespace ConcretePackage<YourHandle>ControllerSinglePageDashboardShoppingFeed;
class Feed extends DashboardPageController { ... }

2) **Routes registered in your package `on_start()` using the same FQCN**

public function on_start()
{
    $r = $this->app->make(ConcreteCoreRoutingRouter::class);

    $f = 'ConcretePackage<YourHandle>ControllerSinglePageDashboardShoppingFeedFeed::view';
    $r->get('/dashboard/shopping_feed/feed', $f);
    $r->get('/dashboard/shopping_feed/feed/add', $f);
    $r->get('/dashboard/shopping_feed/feed/edit/{id}', $f)->setRequirements(['id' => 'd+']);
}

3) **Verify and flush**
– CLI: `./concrete/bin/concrete5 c5:routing:list | grep shopping_feed` → you should see your three GET routes.
– Clear cache.

If the routes **don’t** show up, your `on_start()` isn’t running (package not installed/enabled) or FQCN mismatch.

## One more common gotcha in your installer
You add `/dashboard/shopping_feed/view` and sometimes redirect the parent to it. If your main nav is taking you to `/dashboard/shopping_feed` but you only built the controller under `/dashboard/shopping_feed/feed`, make sure you’re actually visiting the **feed** page URLs:

– Go directly to:
`/index.php/dashboard/shopping_feed/feed/add`
`/index.php/dashboard/shopping_feed/feed/edit/2`

If those work with `/index.php/` but not without, it’s a Pretty URLs issue (less common in dashboard, but worth checking).

## TL;DR
– **Guaranteed fix right now:** add `add()` and `edit($id)` proxies that call `view()`. No routes required.
– If you want routing-only: ensure FQCN matches controller location, register in `on_start()`, confirm with `c5:routing:list`, clear cache.

If you paste your controller namespace line (first few lines of the file) and where the file lives, I can pinpoint the exact mismatch.

User
okay now how to change page title to add feed/edit feed
Assistant
Perfect 👍 now that your `/dashboard/shopping_feed/feed/add` and `/edit/{id}` work and all hit the same `feed.php` view, you just need to set the **page title dynamically**.

In Concrete CMS, a Single Page (DashboardPageController) lets you override the page title with:

$this->set('pageTitle', 'My Title');

…and in the view template use “ instead of hard-coded “.

### 1) Controller: set the title
In your controller (`feed.php`):

public function view($id = null)
{
    $path = rtrim((string)$this->request->getPath(), '/');
    $isAdd  = (bool)preg_match('#/feed/add$#', $path);
    $isEdit = (bool)preg_match('#/feed/edit/d+$#', $path);

    $id = $id ?: (int)($this->request->query->get('id') ?? 0);

    if ($isAdd) {
        $mode = 'add';
        $title = t('Add Feed');
    } elseif ($isEdit || $id > 0) {
        $mode = 'edit';
        $title = t('Edit Feed');
    } else {
        $mode = 'add';
        $title = t('Feed');
    }

    $this->set('mode', $mode);
    $this->set('pageTitle', $title);

    // load feed record if needed...
}

### 2) View file: use `$pageTitle`
In `/single_pages/dashboard/shopping_feed/feed.php`:

<?php
defined('C5_EXECUTE') or die("Access Denied");
/** @var string $pageTitle */
?>

<div class="ccm-ui">
  <h2><?= h($pageTitle) ?></h2>

  <!-- your form here -->
</div>

### 3) Dashboard header bar (optional)
If you want the **browser page title** (top “ in admin header) to also change, you can call:

$this->set('pageTitle', $title);
$this->setPageTitle($title);

Or override `getPageTitle()` in your controller:

public function getPageTitle()
{
    return $this->get('pageTitle') ?: t('Feed');
}

✅ Result:
– `/dashboard/shopping_feed/feed/add` → heading: **Add Feed**
– `/dashboard/shopping_feed/feed/edit/2` → heading: **Edit Feed**
– `/dashboard/shopping_feed/feed` → heading: **Feed**

Do you also want the **breadcrumb / top dashboard bar title** (not just your “) to show “Add Feed” vs “Edit Feed”?

Leave a Comment

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