alpham8
Members-
Posts
5 -
Joined
-
Last visited
Everything posted by alpham8
-
first real world module: a few questions
alpham8 replied to alpham8's topic in Module/Plugin Development
I think it's not good to send the headers that early. But I'll give it a try I've tried readonly and disabled with ->attr(...) method and direct object property, both doesn't work, it's still editable. As I read the the stack trace, it fails at this line in the module class, because this is line 154 in GsSandbox.module: $template = $tpl->get('basic-page'); ... you can find the whole code of this method in my entry post. Yes thank you, that was the issue: Now I don't need to save it separately. -
first real world module: a few questions
alpham8 replied to alpham8's topic in Module/Plugin Development
And of course: Can I use the ClassLoader to load my few classes? It's PSR standard autoload-able -
Hi, I've just made my first real world module 🙂 It's about importing pages from an external API, that uses a special "push" URL where the module listens on. If this special URL gets called, then it does its magic. The module basically replaces a custom field and saves it on the page(s) with the incoming data from the foreign API. And on before saving module configuration, we try to retrieve the credential challenge from the other's side. So, here are my questions: In reference to this new feature article: https://processwire.com/blog/posts/pw-3.0.173/ How can we set HTTP response codes like 400, 404, 500, etc. in an JSON API? And how can we set other response headers like "Content-Type: application/json+problem"? I'm using the new configuration class, i. e. like so: <?php namespace ProcessWire; class GsSandboxConfig extends ModuleConfig { /** * default options for our configurations, if not set */ private const DEFAULT_OPTIONS = [ 'api_key' => '', 'secret' => '', 'testpage' => 'testpage-policy', 'template' => self::TPL_NAME, 'parent' => 'home', ]; /** * It defines the ProcessWire HTML-like Inputfield* class and its properties, the config names should match the DEFAULT_OPTIONS constant. */ private const CONFIG_FIELD_PROPS = [ 'api_key' => ['type' => 'InputfieldText', 'props' => ['label' => 'API Key', 'columnWidth' => 50, 'notes' => 'The API key from your account.', 'required' => true]], 'secret' => ['type' => 'InputfieldText', 'props' => ['label' => 'Secret', 'columnWidth' => 50, 'notes' => 'The secret (PLEASE DO NOT CHANGE IT)', 'required' => false]], 'testpage' => ['type' => 'InputfieldText', 'props' => ['label' => 'site name testpage (Englich)', 'columnWidth' => 50, 'notes' => 'Enter a different name from "testpage" here, if applicable', 'required' => false]], 'template' => ['type' => 'InputfieldText', 'props' => ['label' => 'Template for the pages', 'columnWidth' => 50, 'notes' => 'Enter the template to be used by the text pages here.', 'required' => false]], 'parent' => ['type' => 'InputfieldText', 'props' => ['label' => 'Parent page for the pages', 'columnWidth' => 50, 'notes' => 'Enter the parent page from which the text pages for children\'s pages should be.', 'required' => false]], ]; public const SANDBOX_ENDPOINT_URL = '/sandbox/'; /** * needed for the server API Call to identify the version that we are using */ public const SANDBOX_VERSION_NUMBER = '1.0.3'; public const SANDBOX_REPLACEMENT_VAR = 'sandbox_text'; public const TPL_NAME = 'legal-sandbox'; public const LOG_CHANNEL_FILE = 'sandbox'; public const TEMPLATE_FIELDGROUP_NAME = 'funny-tpl'; public function getDefaults() { return self::DEFAULT_OPTIONS; } public function getInputfields() { $fields = parent::getInputfields(); foreach ($this->getDefaults() as $configPropName => $propValue) { if (isset(self::CONFIG_FIELD_PROPS[$configPropName])) { $f = $this->wire('modules')->get(self::CONFIG_FIELD_PROPS[$configPropName]['type']); $f->attr('name', $configPropName); $f->attr('value', $propValue); foreach (self::CONFIG_FIELD_PROPS[$configPropName]['props'] as $fieldPropName => $fieldPropValue) { $f->{$fieldPropName} = $fieldPropValue; } $fields->add($f); } } return $fields; } } Is it possible to define readonly / not editable fields in module config? Is it possible to build a select based on all existing templates or by a filter to avoid to let the user input something that might be wrong? Corresponding to the select menu for templates: Can we also filter out templates where pages or fields not allowed? 3. It was hard for me to create a new page with no parent page - it should be on the top level (so that it's possible to have URLs like this: www.domain.com/funnypage/). I've tried many different approaches. However, this one works. At the first call, it will fail with an Exception, but on the second call it works (reproducible): public function createTextPage(string $name): Page { $config = $this->wire('modules')->getConfig('GsSandbox'); $pages = $this->wire('pages'); $db = $this->wire('database'); $tpl = $this->wire('templates'); $template = $tpl->get($config['template']); if (!$template instanceof Template) { $template = $tpl->get('basic-page'); } $parent = $pages->findOne('name=' . $config['parent']); if ($parent instanceof NullPage) { $parent = $pages->findOne('name=basic-page'); } $p = $pages->new([ 'template' => $template, 'parent' => '/', 'title' => $name, 'name' => $name, 'path' => '/' . $name, ]); $f = $this->wire('fields')->get(GsSandboxConfig::SANDBOX_REPLACEMENT_VAR); $p->{GsSandboxConfig::SANDBOX_REPLACEMENT_VAR} = $f; $p->save(); return $p; } On the first try, I'll get this Exception (with stack trace): Method Page::localPath does not exist or is not callable in this context, Stack Trace: #0 /wire/core/Page.php(1512): Wire->___callUnknown() #1 /wire/core/Wire.php(419): Page->___callUnknown() #2 /wire/core/WireHooks.php(968): Wire->_callMethod() #3 /wire/core/Wire.php(484): WireHooks->runHooks() #4 /wire/core/Wire.php(487): Wire->__call() #5 /wire/modules/PagePaths.module(451): Wire->__call() #6 /wire/modules/PagePaths.module(82): PagePaths->updatePagePaths() #7 /wire/core/WireHooks.php(1094): PagePaths->hookPageMoved() #8 /wire/core/Wire.php(484): WireHooks->runHooks() #9 /wire/core/PagesEditor.php(787): Wire->__call() #10 /wire/core/PagesEditor.php(478): PagesEditor->savePageFinish() #11 /wire/core/Pages.php(840): PagesEditor->save() #12 /wire/core/Wire.php(416): Pages->___save() #13 /wire/core/WireHooks.php(968): Wire->_callMethod() #14 /wire/core/Wire.php(484): WireHooks->runHooks() #15 /wire/core/PagesEditor.php(117): Wire->__call() #16 /wire/core/Pages.php(975): PagesEditor->add() #17 /wire/core/Wire.php(416): Pages->___new() #18 /wire/core/WireHooks.php(968): Wire->_callMethod() #19 /wire/core/Wire.php(484): WireHooks->runHooks() #20 /site/modules/GsSandbox/GsSandbox.module(154): Wire->__call() #21 /site/modules/GsSandbox/GsSandboxHooker.php(315): GsSandbox->createTextPage() #22 /site/modules/GsSandbox/GsSandboxHooker.php(228): GsSandboxHooker->writeToPage() #23 /site/modules/GsSandbox/GsSandboxHooker.php(105): GsSandboxHooker->importFunnyPage() #24 /wire/core/WireHooks.php(1094): GsSandboxHooker->urlListener() #25 /wire/core/Wire.php(484): WireHooks->runHooks() #26 /wire/modules/Process/ProcessPageView.module(265): Wire->__call() #27 /wire/modules/Process/ProcessPageView.module(116): ProcessPageView->renderNoPage() #28 /wire/core/Wire.php(416): ProcessPageView->___execute() #29 /wire/core/WireHooks.php(968): Wire->_callMethod() #30 /wire/core/Wire.php(484): WireHooks->runHooks() #31 /webserver/document-root/public/index.php(55): Wire->__call() #32 {main} 4. How can we log messages that the user will see in the admin? I'm currently doing this, but this only logs in the separated text file and somehow in an unfindable "session": private function sendError(string $msg): void { $this->wire('log')->save( GsSandboxConfig::LOG_CHANNEL_FILE, 'An error occured during init of the module: ' . $msg ); $this->session->message('An error occured during init of the module: ' . $msg); } 5. How is it possible to write on HTML editor (admin only) multi-language pages with the above method? Because we'd like to use link alternate language meta tag to reference to the corresponding language versions of this page. I know that you need your own language mapping, since it's totally free to define it ProcessWire, but an example would be helpful and highly appreciated! So this is basically my hooker class (a dedicated class where all hooks are placed and the business logic happens, stripped down to the necassary show case parts), which is added as singleton in the Module::init method: <?php declare(strict_types=1); namespace ProcessWire; require_once __DIR__.DIRECTORY_SEPARATOR.'GsSandbox.module'; require_once __DIR__.DIRECTORY_SEPARATOR.'GsSandboxConfig.php'; class GsSandboxHooker extends Wire { private static ?GsSandboxHooker $instance = null; private ?Module $mainModule = null; private bool $wasInSaveHook = false; private const TO_VERIFY_PAGES = [ 'funnypage', 'anotherpage' ]; public static function getInstance(Module $module): GsSandboxHooker { if (null === self::$instance) { self::$instance = new GsSandboxHooker($module); self::$instance->setMainModule($module); } return self::$instance; } public function addOurHooks(): void { $this->addHook(GsSandboxConfig::SANDBOX_ENDPOINT_URL, $this, 'urlListener'); $this->addHook('Modules::saveConfig', $this, 'onBeforeSaveConfiguration'); } public function urlListener(HookEvent $event): array { $input = $this->wire('input'); if(!$this->validate()) { $this->log("[IMPORT FEHLGESCHLAGEN]"); exit; } $data = ['status' => 200, 'message' => 'Dokumente erfolgreich importiert']; try { // ... } catch (\Throwable $e) { header("HTTP/1.1 500 Server Error"); header('Content-Type: application/json'); $data = [ 'status' => 500, 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile(), ]; } return $data; } public function onBeforeSaveConfiguration(HookEvent $event) { $class = $event->arguments(0); $assocData = $event->arguments(1); if ($class !== $this->mainModule->className || $this->wasInSaveHook()) { return; } $apiKey = strval($assocData['api_key']); if (!$apiKey) { $this->sendError('No API key specified, abort!'); return; } $this->setWasInSaveHook(true); // update parent and template on each, if necessary: $this->verifyAndUpdatePages($assocData); // business logic here, save your stuff, request the API, etc. // save back everything: $event->arguments(1, $assocData); $this->saveModuleConfig($assocData); } private function saveModuleConfig(array $data): void { $this->wire('modules')->saveConfig('GsSandbox', $data); } private function verifyAndUpdatePages(array $assocData, array $toVerifyPages = self::TO_VERIFY_PAGES): void { $pages = $this->wire('pages'); $db = $this->wire('database'); $tpl = $this->wire('templates'); foreach ($toVerifyPages as $p) { $shouldSave = false; $page = $pages->findOne('name=' . $p); if ($page instanceof NullPage) { if (!$this->writeToPage($p, 'init')) { continue; } $page = $pages->findOne('name=' . $p); } if ($page->template !== $assocData['template']) { $innerTpl = $tpl->get($assocData['template']); if ($innerTpl instanceof Template) { $page->template = $innerTpl; $shouldSave = true; } } // TODO: bugfix parent page association: // if ($page->parent->name !== $assocData['parent']) { // $parentPage = $pages->findOne('name=' . $db->escapeTableCol($assocData['parent'])); // if ($parentPage instanceof Page) { // $page->parent = $parentPage; // $shouldSave = true; // } // } if ($shouldSave) { $page->save(); } } } public function setMainModule(Module $module): void { $this->mainModule = $module; } public function getMainModule(): ?Module { return $this->mainModule; } public function wasInSaveHook(): bool { return $this->wasInSaveHook; } public function setWasInSaveHook(bool $wasInSaveHook): void { $this->wasInSaveHook = $wasInSaveHook; } } A few questions to this: 6. At installation of this module the on before configuration saving hook gets fired, otherwise I couldn't explain why it creates the page twice. Is it possible to exclude also this call in my hook? 7. When we retrieve the new configuration values BEFORE it saves, why we can't manipulate it for saving? I need the saveModuleConfig approach, otherwise it doesn't get saved. Any help to this or tips would be highly appreciated. PS: We're using ProcessWire 3.0.229 on PHP 8.2.24 Thanks in advice 🙂
-
Hi again, and sorry for my late reply. Your answers have helped me well, thank you! 🙂 I will try to go through every answer: Yes, that's true, I need autoload here. This was simply to my missing knowledge of what you can code in a template. My question was how can we access those variables, that are returned by module methods, if we imagine, that we are currently inside site/templates/home.php, an stripped down example: <?php namespace ProcessWire; // Template file for “home” template used by the homepage /** @var Page $page */ /** @var Pages $pages */ /** @var Config $config */ ?> <?php include '_header.php'; ?> <section id="home" class="overflow-clip"> <!-- Projekte --> <div class="position-relative d-flex mb-120px-xs-70px py-210px pb-0"> <div class="container"> <div class="row d-flex"> <div class="col-7 col-md-5"> <div class="pb-15px heading-large"> <?php echo $page->heading_slider; ?> </div> </div> </div> </div> </div> <a href="<?php echo $pages->get(1466)->url; ?>#heading" class="inner"> <img src="<?php echo $config->urls->templates; ?>media/images/icon.svg" alt="icon rocket"> <div class="text-center">Our advantages</div> </a> </section> <?php include '_footer.php'; ?> So, at home.php template we can use there $page, $pages, $config and every other variable that a module might inject. And of course, from page with dot notation every field that this page have. There are two types of fields: system-wide fields, that are loaded on every request and page-fields which are only loaded when the concrete page is loaded and those fields on this page are requested to be loaded. Fields are always copied into a page, so there's not a reference or foreign key relation to the origin field. Thank you, got it! I've done this. Unfortunately, this provides us relative URLs. I have had needed absolute URLs. However, I managed to use the relative URL here. That was exactly, what I was looking for, thank you so much! 🙂 I've used this multiple times. I needed to get used with the ProcessWire's own Callable syntax, but now it's working. Thank you again all, this topic can be closed.
-
Hi, I'm trying to get some basic knowledge of Processwire module development. I've read the beginner guides and I've done a lot of research. My current use case is to extend a custom admin template for pages with an update button in the "settings" tab of this template. The update button should then update the page's data through an external API on this page using its metadata that this template type has when you edit the page in the admin. Here's the shortened PHP code (yes, I've build own classes for the hooks) of what I trying to achieve that comes along with a few problems: <?php namespace ProcessWire; class TestUpdate extends Process implements ConfigurableModule { public const XHR_PAGE_NAME = 'test-update'; public const JSON_TPL_NAME = 'json'; public static function getModuleInfo() { return [ 'title' => 'My Admin Extender', 'summary' => 'An admin module to easily bring test data up-to-date', 'version' => '0.0.1', 'permission' => 'test-update', 'permissions' => [ 'test-update' => 'View the button in the template', 'test-update-save' => 'Permission to perform the actual update from API', ], 'icon' => 'table', 'author' => 'alpham8', 'requires' => 'UpdatePlugin>=0.0.1, ProcessWire>=3.0.133', 'page' => [ 'name' => self::XHR_PAGE_NAME, 'title' => 'update-product', 'template' => 'admin' ] ]; } public function ready() { /** @var Page $page */ $page = $this->wire('page'); // add helper notices about ProCache to page and template editors if (static::isTemplate('admin', $page)) { $this->addHookAfter('ProcessPageEdit::buildFormSettings', $this, 'hookPageEdit'); } } public function hookPageEdit(HookEvent $event) { /** @var WirePageEditor $process */ $process = $event->object; $page = $process->getPage(); if (static::isTemplate('product', $page)) { // TODO: maybe add $this->user->isSuperuser() check $form = $event->return; /** @var InputfieldMarkup $field */ $field = $this->modules->get("InputfieldMarkup"); $info = HaroImportProductPages::getModuleInfo(); $field->label = $info['title']; $field->icon = $info['icon']; /** @var InputfieldButton $btn */ $btn = $this->modules->get('InputfieldButton'); $btn->attr('value', 'update product data'); if ($page->hasField('param1') && $page->hasField('param2')) { $btn->attr( 'href', '#' ); $btn->addClass('update-link'); $btn->attr('data-param1', $page->get('param1')); $btn->attr('data-url', 'https://' . $_SERVER['HTTP_HOST'] . '/processwire/test-update/save/'); } $btn->icon = 'refresh'; $btn->attr('id+name', 'UpdateProductPagesBtn'); $btn->label = 'Update product data'; $btn->collapsed = Inputfield::collapsedNever; $field->collapsed = Inputfield::collapsedNever; $field->attr('value', $btn->render()); $form->add($field); } } /** * Does user have permission? * * @param string $name * @return bool * @throws WireException */ protected function hasPermission(string $name): bool { $user = $this->wire()->user; if ($user->isSuperuser()) { return true; } return $user->hasPermission($name); } public function execute(): array|string { return 'test1'; } public function executeSave(): array|string { $input = $this->wire()->input; $testparam = $input->post('testparam'); $result = false; /** @var UpdatePlugin $module */ $module = $this->wire->modules->get('UpdatePlugin'); // possibly product number check: if ($testparam !== '' && is_numeric($testparam) && \mb_strlen($testparam, 'UTF-8') >= 4) { $result = $module->writeDataToPage($testparam); } return json_encode([ 'testparam' => $testparam, 'success' => $result === 1, ]); } } My problems or questions are: 1st The ready method doesn't get called in an admin module, instead, I need to place it in another module like this: <?php namespace ProcessWire; class UpdatePlugin extends Wire implements Module { public static function getModuleInfo() { return [ 'title' => 'UpdatePlugin', 'version' => '0.0.1', 'summary' => 'Summary for UpdatePlugin ...', 'author' => 'alpham8', 'icon' => 'table', 'autoload' => true, 'requires' => 'ProcessWire>=3.0.123', ]; } public function init() { $scriptUrl = $this->urls->$this . 'assets/product-update.js'; $this->config->scripts->add($scriptUrl); } public function ready() { /** @var Page $page */ $page = $this->wire('page'); // add helper notices about ProCache to page and template editors if (static::isTemplate('admin', $page)) { $this->addHookAfter('ProcessPageEdit::buildFormSettings', $this, 'hookPageEdit'); } } // [see above] ... } 2nd Isn't there a way to just build an ajax request that returns plain JSON without that page / template skeleton? As far as I understood it, a page is mandatory in Processwire for this. That would be fine, if we can hide the page in the admin's navbar by using css. I thought that might be possible with creating an own template that just outputs JSON, right? But how to do a template without a parent one? 3rd How can I bring or access variables from modules in pages? Just by calling it without the knowledge that they are exist on the concrete (PHP) page? 4th Isn't there a way to get the absolute URL out of $config->urls ? I'd like to avoid that insecure $_SERVER['HTTP_HOST'] call. We're currently on ProcessWire 3.0.210. An update is planned after that modules. Any help to that above question would be great. Thanks in advice, alpham8