Package setup guide

User
Assistant
Opened a canvas with a clean, step‑by‑step Marketplace‑ready plan and exact code you can paste. Want me to start applying these steps to your current files (controller, routes, API, task) and give you final diffs next?
User
but now
/index.php/google-merchant-feed/feed-09713944
give page not found after shift routes to config
Assistant
That 404 is because package routes aren’t auto‑loaded from `config/routes.php`. In Concrete CMS 9, **packages should register routes via a `RouteList` class and call it from `on_start()`**.

Do this:

1) Create `packages/shopping_feed_manager/src/RouteList.php`

<?php
namespace ConcretePackageShoppingFeedManager;

use ConcreteCoreRoutingRouteListInterface;
use ConcreteCoreRoutingRouter;

class RouteList implements RouteListInterface
{
    public function loadRoutes($router)
    {
        // Group under /shopping_feed for your API
        $router->buildGroup()
            ->setPrefix('/shopping_feed')
            ->routes(function (Router $group) {
                $group->get('/api/feeds',
                    'Concrete\Package\ShoppingFeedManager\Api\FeedApi::view');
                $group->post('/api/delete/{id}',
                    'Concrete\Package\ShoppingFeedManager\Api\FeedApi::delete');
                $group->post('/api/clone/{id}',
                    'Concrete\Package\ShoppingFeedManager\Api\FeedApi::cloneFeed');
            });

        // Public feed endpoint (no prefix)
        $router->get('/google-merchant-feed/{feedSlug}',
            'Concrete\Package\ShoppingFeedManager\Controller\MerchantFeed::generateXml');
    }
}

2) Call it from your package controller `on_start()`:

use ConcretePackageShoppingFeedManagerRouteList;

public function on_start()
{
    // ... your existing asset registration etc.
    $router = $this->app->make('router');
    (new RouteList())->loadRoutes($router);
}

3) Delete the `config/routes.php` you added (it won’t be picked up for packages). Clear cache: Dashboard → System & Settings → Optimization → **Clear Cache**.

Now `/index.php/google-merchant-feed/feed-09713944` should resolve again (with or without `index.php`, depending on pretty URLs).

Docs for this exact pattern (“Including Routes in Packages”) are here. citeturn1view0

If it still 404s, double‑check:
– The controller class `ConcretePackageShoppingFeedManagerControllerMerchantFeed` exists and `generateXml($feedSlug)` is **public** and returns a Response.
– Your package’s `composer.json` (or `$pkgAutoloaderRegistries` for older setups) autoloads `src/`. citeturn1view0

Want me to paste a minimal `MerchantFeed` controller stub that returns a test string so you can verify the route?

User
its fixe dnow
but when delete/clone
"errors": [
"Class "Concrete\Package\ShoppingFeedManager\Api\FeedApi" does not exist"
],
Assistant
That error means the `FeedApi` class isn’t being autoloaded (or it’s in the wrong namespace/path).

Do these two things:

# 1) Create the class in the right path + namespace
**File:** `packages/shopping_feed_manager/src/Api/FeedApi.php`

<?php
namespace ConcretePackageShoppingFeedManagerApi;

use ConcreteCoreHttpResponseFactoryInterface;
use ConcreteCoreSupportFacadeApplication;
use ConcreteCoreValidationCSRFToken;

class FeedApi
{
    public function view()
    {
        $app = Application::getFacadeApplication();
        return $app->make(ResponseFactoryInterface::class)->json(['ok' => true]);
    }

    public function delete($id)
    {
        $app = Application::getFacadeApplication();
        $token = $app->make(Token::class);
        if (!$token->validate('shopping_feed_delete')) {
            return $app->make(ResponseFactoryInterface::class)->forbidden($token->getErrorMessage());
        }
        // TODO: delete logic...
        return $app->make(ResponseFactoryInterface::class)->json(['deleted' => (int) $id]);
    }

    public function cloneFeed($id)
    {
        $app = Application::getFacadeApplication();
        $token = $app->make(Token::class);
        if (!$token->validate('shopping_feed_clone')) {
            return $app->make(ResponseFactoryInterface::class)->forbidden($token->getErrorMessage());
        }
        // TODO: clone logic...
        return $app->make(ResponseFactoryInterface::class)->json(['cloned' => (int) $id]);
    }
}

# 2) Ensure PSR‑4 autoloading is set up
**Option A (recommended): `composer.json` in the package root**

{
  "name": "your-vendor/shopping-feed-manager",
  "type": "concrete5-package",
  "require": {
    "php": ">=7.4",
    "concrete5/core": ">=9.0"
  },
  "autoload": {
    "psr-4": {
      "Concrete\Package\ShoppingFeedManager\": "src/"
    }
  },
  "extra": { "concrete5-package": { "handle": "shopping_feed_manager" } }
}

Then run in `packages/shopping_feed_manager/`:

composer dump-autoload

**Option B (no Composer): declare autoload registries in your package controller**
Add this method to your `packages/shopping_feed_manager/controller.php`:

public function getPackageAutoloaderRegistries()
{
    // Map the namespace root to /src
    return [
        'src' => [
            'Concrete\Package\ShoppingFeedManager' => 'Concrete\Package\ShoppingFeedManager'
        ],
    ];
}

Finally: **Clear cache** (Dashboard → System & Settings → Optimization → Clear Cache) and retry the delete/clone calls.

If you want, I can drop this `FeedApi.php` into your canvas and add the `getPackageAutoloaderRegistries()` snippet to your controller so you can copy-paste directly.

User
stil get
{
"error": true,
"errors": [
"Class "Concrete\Package\ShoppingFeedManager\Api\FeedApi" does not exist"
],
evend create above
Assistant
Got it—this is almost always an autoloading issue. Do these three quick fixes (in order), then clear cache and retry:

## 1) Ensure the file + namespace are exactly right
**Path:**
`packages/shopping_feed_manager/src/Api/FeedApi.php`

**Top of file must be:**

<?php
namespace ConcretePackageShoppingFeedManagerApi;

use ConcreteCoreHttpResponseFactoryInterface;
use ConcreteCoreSupportFacadeApplication;
use ConcreteCoreValidationCSRFToken;

class FeedApi
{
    public function view() { /* ... */ }
    public function delete($id) { /* ... */ }
    public function cloneFeed($id) { /* ... */ }
}

## 2) Add the (core) package autoloader in your package controller
If you’re **not** using Composer inside the package (i.e., no `vendor/autoload.php` present or you didn’t run `composer dump-autoload`), add this **property** to `packages/shopping_feed_manager/controller.php`:

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

    // 👇 This tells Concrete to PSR-4 autoload everything under /src
    protected $pkgAutoloaderRegistries = [
        'src' => [
            'Concrete\Package\ShoppingFeedManager' => 'Concrete\Package\ShoppingFeedManager',
        ],
    ];

    // ...
}

> Note: this is a **property**, not a method.

If you **are** using Composer in the package, make sure you:
– Have `composer.json` with:

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

– Ran `composer dump-autoload` in `packages/shopping_feed_manager/`
– (Optional) keep your existing `on_start()` `require __DIR__.'/vendor/autoload.php';`

You can use **either** Composer autoload **or** the `$pkgAutoloaderRegistries` property. Using both is fine too.

## 3) Make the route callback a plain string (remove array callable)
In your RouteList (the one currently open in canvas), change the API routes to string targets (Concrete will instantiate the class for you once autoloading works):

$router->buildGroup()
  ->setPrefix('/shopping_feed')
  ->routes(function (ConcreteCoreRoutingRouter $group) {
      $group->get('/api/feeds',
          'Concrete\Package\ShoppingFeedManager\Api\FeedApi::view');
      $group->post('/api/delete/{id}',
          'Concrete\Package\ShoppingFeedManager\Api\FeedApi::delete');
      $group->post('/api/clone/{id}',
          'Concrete\Package\ShoppingFeedManager\Api\FeedApi::cloneFeed');
  });

// Public route:
$router->get('/google-merchant-feed/{feedSlug}',
    'Concrete\Package\ShoppingFeedManager\Controller\MerchantFeed::generateXml');

Array callables are fine, but using the string form avoids any oddities while you’re debugging autoloading.

## Finally: Clear caches
Dashboard → System & Settings → Optimization → **Clear Cache**
(Or delete `application/files/cache` contents if you’re in dev and know what you’re doing.)

### Quick sanity check route (optional)
Add this temporary route to your RouteList to confirm autoloading:

$router->get('/_debug/feedapi-exists', function() {
    return SymfonyComponentHttpFoundationResponse::create(
        class_exists(ConcretePackageShoppingFeedManagerApiFeedApi::class) ? 'YES' : 'NO'
    );
});

Hit `/index.php/_debug/feedapi-exists`. If it says **YES**, autoloading is fixed—your delete/clone endpoints will work.

If you still see the error after these changes, send me your current `controller.php` header (top ~40 lines) and the exact path + first 5 lines of `src/Api/FeedApi.php`—I’ll spot the mismatch fast.

User
new
Assistant
{"updates":[{"pattern":".*","multiple":false,"replacement":"
Assistant
I added a temporary debug route (`/_debug/feedapi-exists`) that will return **YES** if `FeedApi` autoloads correctly.
Try hitting `/index.php/_debug/feedapi-exists` in your browser—if it prints YES, your API class is loading fine and delete/clone should work.

Do you want me to also add the `$pkgAutoloaderRegistries` property into your package controller so you don’t rely on Composer for autoloading?

Leave a Comment

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