0

I am currently trying to write my 1st Craft plugin.

The goal (simplified): Backend users select a bunch of pages in the CP, and create a PDF. The result is an annual report that's optimized for print (so each year = 1 site / group, and each Craft page = 1 PDF "chapter").

I've used plugin factory for scaffolding, and have the two main pieces together:

PHP: get URL-list from outside, fetch pages via cURL, extract content via xPath, re-arrange HTML-headings (h1 becomes h2 etc.), create a table of contents and finally store the PDF somewhere (mPDF library was used).

Craft CP: Create a sortable pages list with sortable.js, and a button that POSTs the JSON.stringify list with fetch to my plugin controller action.

No matter what I do, the controller responds with server error 500. I've tried cors or no-cors, fiddled around with Apache headers (allow-origin et al), even used a simple old-school form instead of JS, but I was never able to get an HTTP result code 200 instead of 500.

I have tried it locally first, then remote. What I'm trying to do is certainly not rocket science, I guess I either miss something that's not covered in the Craft docs, or have some server settings wrong (or maybe it's documented, but rather kinda "hidden" somewhere I didn't look yet).

tl;dr: Posting data from my plugin CP to the plugin's controller action ends up with server error 500, and I'm stuck here. That's the last stumbling block to get my plugin finished, at least for a rough demo / MVP.

JS inside my custom plugin templates/index.twig:

button.addEventListener('click', event => {
  const newList = el.children;
  let ar = [];
  Array.from(newList).forEach((item) => {
    let url = item.querySelector('a').attributes.href.value;
    ar.push(url);
  })

  const pageSelection = JSON.stringify(ar);
  const target = '/actions/pdf-creator/default/get-page-selection';

  window.csrfTokenName = "{{ craft.app.config.general.csrfTokenName|e('js') }}";
  window.csrfTokenValue = "{{ craft.app.request.csrfToken|e('js') }}";

  let data = {};
  data[csrfTokenName] = csrfTokenValue;
  data["selection"] = pageSelection;

  fetch(target, {
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json, text-plain",
      "CRAFT_CSRF_TOKEN": window.csrfTokenValue,
    },
    method: 'POST',
    mode: 'no-cors',
    body: data
  })
    .then(response => getHeaders(response))
    .then(response => console.log(response))
});

My controller action: (inside myplugin/src/controllers/DefaultController.php)

public function actionGetPageSelection() {
    $this->requirePostRequest();
    $post = Craft::$app->getRequest();
    //    $rawBody = Craft::$app->getRequest()->getRawBody(); // doesn't work either
    $ar = array("status" => "test"); // just for testing
    $json = json_encode($ar);
    return $json; // nothing - server error 500
}

Any hints or tips? Any help is appreciated.

Update:

At one point, there was this error, which is now gone:

-][-][-][error][yii\web\HttpException:400] yii\web\BadRequestHttpException: Invalid JSON data in request body: Syntax error. in /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/web/JsonParser.php:60

I posted the data in body and in the header. The header payload was received successfully and displayed in web.log:

$_SERVER = [
 'HTTP_PAYLOAD' => ...
]

Now I've simplified my post data, and got this error:

[-][795][iaaukc5i9h7vq69afgfs13h0l0rv10se][error][Error] Error: Class 'zeixag\pdfcreator\controllers\Controller' not found in /home/examplecom/www/dev.sitename/zeixag/pdf-creator/src/controllers/PagelistController.php:26
Stack trace:
#0 /home/examplecom/www/dev.sitename/vendor/composer/ClassLoader.php(478): include()
#1 /home/examplecom/www/dev.sitename/vendor/composer/ClassLoader.php(346): Composer\Autoload\includeFile('/home/examplecom/w...')
#2 [internal function]: Composer\Autoload\ClassLoader->loadClass('zeixag\\pdfcreat...')
#3 [internal function]: spl_autoload_call('zeixag\\pdfcreat...')
#4 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Module.php(643): class_exists('zeixag\\pdfcreat...')
#5 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Module.php(602): yii\base\Module->createControllerByID('pagelist')
#6 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Module.php(594): yii\base\Module->createController('')
#7 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Module.php(528): yii\base\Module->createController('pagelist')
#8 /home/examplecom/www/dev.sitename/vendor/craftcms/cms/src/web/Application.php(277): yii\base\Module->runAction('pdf-creator/pag...', Array)
#9 /home/examplecom/www/dev.sitename/vendor/craftcms/cms/src/web/Application.php(586): craft\web\Application->runAction('pdf-creator/pag...', Array)
#10 /home/examplecom/www/dev.sitename/vendor/craftcms/cms/src/web/Application.php(256): craft\web\Application->_processActionRequest(Object(craft\web\Request))
#11 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Application.php(392): craft\web\Application->handleRequest(Object(craft\web\Request))
#12 /home/examplecom/www/dev.sitename/web/index.php(21): yii\base\Application->run()
#13 {main}
2021-09-28 09:16:38 [-][795][iaaukc5i9h7vq69afgfs13h0l0rv10se][info][application] $_GET = [
    'p' => 'cms/actions/pdf-creator/pagelist'
]

While the first error (malformed JSON) made 100% sense to me, this has me baffled.

It seems like Craft assumes my controllers are not in the vendor folder, but in root. Why? I only placed it in root when I installed it via Composer. How can I fix this? I already removed and re-installed the plugin twice.

3
  • 1
    can't you see anything on network response on browser? what about /storage/logs/web.log?
    – user9975
    Sep 27 at 13:58
  • for custom plugin added to composer locally via path, you should composer remove package/name and composer require package/name on command line to reinstall that plugin. have you tried this?
    – user9975
    Sep 28 at 12:13
  • This doesn't answer your question, but given your use case, have you considered using a third-party plugin? I've not tried any of these myself but there's a couple here that sound like they'll do what you need: plugins.craftcms.com/search?q=pdf (plugins.craftcms.com/super-pdf and plugins.craftcms.com/document-helpers specifically) Sep 29 at 8:29
0

There are two distinct problems here, the first with the formatting of your POST request body and the second with the location / routing of your contoller. You were getting the first error because your POST request contained invalid JSON, which means Yii2 returns an error before the request even reaches your controller. When you simplified your request for testing purposes, you probably removed that error, so now the problem is that Yii2 can't reach your controller.

Fixing the POST request

This is only a minor error – you have the Content-Type: application/json header in your request, but you're not actually passing JSON.

let data = {};
data[csrfTokenName] = csrfTokenValue;
data["selection"] = pageSelection;

fetch(target, {
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json, text-plain",
    "CRAFT_CSRF_TOKEN": window.csrfTokenValue,
  },
  method: 'POST',
  mode: 'no-cors',
  body: data
})

Note that data is not a JSON string, but an object with two properties. Only the pageSelection property contains a JSON string. fetch doesn't automatically stringify (encode) data, so when you give it an object, the default Object.prototype.toString method is used to convert it for the request. In your case, this will return [object Object], which is what the server receives. This isn't valid JSON, so you get an error.

The solution is to stringify your object:

body: JSON.stringify(data),

You don't want to encode the URLs to JSON twice, so make sure to remove this line and instead add the array with URls directly to the data object.

const pageSelection = JSON.stringify(ar);

To debug errors like this, using the network tab in your browser console is essential. A quick test will show you the problem. In the console, try this code:

fetch('/test', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: { foo: 'bar'}
})

Now check the network tab, click on the fetch request and check the request tab for that request. It will show you that the body isn't a JSON string, but [object Object].

Fixing the controller routing

The correct setup depends on what you're trying to build – a plugin that can be installed by others using Composer, or a module that is specific to a single site and included in the repository of the site?

  • For a plugin: See How to build a plugin. Make sure to set up your plugin folder according to this guide, and include it with composer using a local path in a test site (as specified in the guide). In the plugin factory, make sure you selected Craft CMS Plugin version 3.x.
  • For a site-specific module: See How to build a module. Your module files go in the modules/ directory and you have to adjust the autoload information in the composer.json of your site so it knows to load modules from that folder. In the plugin factory, make sure you selected Craft CMS Module version 3.x.

If you're having trouble, I recommend not to use the plugin factory. Instead, start from scratch and go through the guide step-by-step. The plugin factory is really a power-user tool and includes more than you need, in particular if you're just starting out.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.