Jump to content

bernhard

Members
  • Posts

    6,674
  • Joined

  • Last visited

  • Days Won

    367

Everything posted by bernhard

  1. But I was ? And I tried to explain why... IMHO not That's what I was trying to explain. I'm using custom page classes in an object oriented way (similar to PW modules). And that's great because you have all your code pieces in one place - namely in one single php file of your custom page class. Let's say we build a webpage about cats. Step one: Create a custom page class and place it in /site/classes <?php namespace ProcessWire; class Cat extends Page { } Now all cats in our system are of type "Cat", and that's great! Now we want a custom headline for every cat on our frontend based on the title field, easy: <?php namespace ProcessWire; class Cat extends Page { public function headline() { if(!$this->title) return "This cat has no name"; return "Details for Cat '{$this->title}'"; } } In your template <h1><?= $page->headline() ?></h1> And that you'll do for any kind of business logic your application needs. Now what if we wanted to manage thousands of cats and we wanted every cat have a unique 4-letter page name instead of one based on the title (to prevent ugly suffix urls like catname-1, catname-2 etc)? We could do that easily with a hook. In the past one might have put the hook into ready.php and after time your ready.php grows and grows and grows and some day you totally lose control over your hooks and over your software. Not if you place them where they belong: Into the page class to the cat object: <?php namespace ProcessWire; class Cat extends Page { public function init() { $tpl = "template=cat"; $this->addHookAfter("Pages::saveReady($tpl,id=0)", $this, "onCreate"); } public function headline() { if(!$this->title) return "This cat has no name"; return "Details for Cat '{$this->title}'"; } public function onCreate($event) { $page = $event->arguments(0); $page->name = $event->pages->names()->uniqueRandomPageName(4); } } That's how you'd attach a hook in an autoload module, and that's what I suggest: Structure your custom page class just like an autoload pw module. Apply the same principles and your code will get better readable, better understandable and better maintainable. Now the only problem ist, that init() of the page class does not get called like it gets called in an autoload module. Neither does ready(). But it's simple to overcome this - just place this in init.php $cat = new Cat(); $cat->init(); This triggers the init method of your cat on INIT of PW and it attaches all hooks of your cat page class as if they where placed in init.php; The same technique can be used for ready() in ready.php; That does load one instance of cat into memory though. That's a little overhead. IMHO it's worth the overhead, but I can only guess that the overhead is very small... Maybe one could also use loaded() for that, I don't know and I'd have to investigate on this. Maybe someone can try and share their findings ?
  2. This is something where PW is a little... special. The problem is that it decides wheter a page is viewable or not from the existance of /site/templates/yourtemplate. That means you don't have those files under control of your module. Copying the file on install is not ideal as well, because you end up getting a static copy that will not update on updates. You could of course copy your files also on every update, but that approach has 2 other problems: Development gets complicated because you need to copy back and forth your files while testing them. And second you might modify the template file in /site/templates because you (or someone else) thinks that this is the correct file and on the next update you lose your changes. Or you have the file under control of git and you get conflicts there... To solve all that problems I have introduced the includeViews() method in RockMigrations: https://github.com/BernhardBaumrock/RockMigrations/blob/636cf1eca8f8f54683551ddea08ebc576c160124/RockMigrations.module.php#L305-L339 It will create a file in /site/templates/foo.php and you can happily code in /site/modules/YourModule/views/foo.php <?php namespace ProcessWire; // DONT CHANGE THIS FILE // it is created automatically via RockMigrations include($config->paths->root.'site/modules/YourModule/views/foo.php');
  3. Just did an update from 4.20.15 to 4.21.51 and get this error. Should be an easy fix I guess. Thx ?
  4. I don't understand what you mean by "frontend" page classes ?
  5. Sad to hear that! But I'm sure you'll find help ? Other important informations: Server environment (Linux I guess)? Local environment? Also Linux? Windows? XAMPP? Laragon? PHP/MySQL Versions? What is the error message you see? Maybe some helpful informations in /site/assets/logs ?
  6. Are you sure you want to use Pages::save() ? This does NOT mean that the page has been saved. For this we have the Pages::saved() hook! You are hooking into saving a single page field value, eg $page->save('myfield', 'myvalue');
  7. I already showed an example here https://processwire.com/talk/topic/21212-rockmigrations-easy-migrations-from-devstaging-to-live-server/?do=findComment&comment=212496 and here https://processwire.com/talk/topic/25342-custom-classes-for-page-objects-the-discussion/?do=findComment&comment=212563 of course you can use $tpl = "template=yourtemplate" instead of using the class constant.
  8. Please see my post here That's correct behaviour. You are attaching the hook in your custom page class, but the hook you are attaching belongs to "Pages" and not to the custom page class. If you attach a Pages::saveReady hook, it will always fire on Pages::saveReady having the saved page as arguments(0) and therefore you need to check if that page is your custom pageclass or not. BTW I'd recommend doing this instead of $this != $p $page = $event->arguments(0); if(!$page instanceof self) return; I've had problems using $page != $this when some properties of my page have changed and therefore the check returned false even though it was an instance of my custom pageclass. "instanceof" should always work as expected. You could also just check for $page->template if you prefer...
  9. In Page.php we have the hookable loaded method: /** * For hooks to listen to, triggered when page is loaded and ready * * #pw-hooker * */ public function ___loaded() { } It is called on these spots in PW: So this means that loaded() is triggerd on a page find operation: I'm not 100% sure if that means that it is called on every page find or if PW's internal cache somehow plays a role, but as long as the page is not loaded, the loaded hook will not trigger. If you look at the docs of modules you see that init() is called immediately when the module is loaded and ready() is called when the api is ready. This can make an important difference sometimes where hooks work in init() but not in ready() and vice versa. Page objects don't have these methods though. These are all the hookable methods of the Page baseclass: Additionally you have hookable methods in PagePermissions.module: And several others ? But no init() and ready(). Because init() and ready() are a concept of PW modules: My point is that loaded() of a page might only be triggered in very special cases. Imagine a module that loads a page only on uninstall() of the module. Then the loaded() event of your pageClass would happen very late in the request. But if you use custom page classes in an object oriented way then they become much more like a PW module and then I think it makes sense to stick to the PW module naming convention and that means that init() is triggered on module/pageclass init - just like you had the code in init.php. That IMHO does also mean, that this code does get executed on EVERY request. It does NOT mean, that the code is executed only ONCE - that's a matter of the "singular" setting of a module. See examples below. Code in ready() on the other hand is just like code in ready.php - it fires once, but later in the process, when the API is ready and all other init() methods of all modules and pageclasses have been triggered. Try this /site/modules/Test.module.php <?php namespace ProcessWire; class Test extends WireData implements Module { public static function getModuleInfo() { return [ 'title' => 'Test', 'version' => '0.0.1', 'summary' => 'Your module description', 'autoload' => false, 'singular' => false, 'icon' => 'smile-o', 'requires' => [], 'installs' => [], ]; } public function init() { bd('test init'); } public function ready() { bd('test ready'); } } Install the module and then call it from the tracy console: It only fires init() but not ready() - because when loading the module PW's ready event has already been triggered. Now make that module autoload, do a modules refresh and fire the console code again: You see the difference? ? Now just for fun we make the module "singular": If I happen to need something to execute on every load of the pageClass I use the class constructor: This means that my custom pageclass always has the correct template and does always live under the correct parent in the pagetree. As you can see I define all my fields as class constants. This makes the code so much more readable and also makes typos nearly impossible. It might feel like overhead, but actually it makes me a lot more efficient as well: So I'm actually using most if not all of my page classes as something like a singular autoload module. Overhead? Maybe a little. Problem? I think/hope no ? Hope that helps. Suggestions for improving this further always welcome ?
  10. I'm all in on pageClasses and it has totally changed the way I'm building apps with processwire ? I'm even rewriting old projects step by step to make my code better readable and maintainable. The great thing is that this does not break any existing code - you can move hooks step by step into the new pageClasses as dedicated methods of that pagetype. I have showcased my way of doing what you are asking for in 2 posts starting here: https://processwire.com/talk/topic/21212-rockmigrations-easy-migrations-from-devstaging-to-live-server/?do=findComment&comment=212491 I'd recommend triggering a method called "ready" only on the PW "ready" event, not on "loaded" - that's something different and if you create a method called "ready" that is actually triggered on "loaded" you'll likely produce code that might at some day produce hard to find bugs... Same goes for "init". I'd be interested what Ryan says to this topic. The reason why ready() and init() of custom page classes are not triggered is obviously because custom page classes are not autoload modules. They are requested on demand and you can therefore not tell when or if they get loaded. I guess that's to minimize the footprint of every request as much as possible. In my modules I simply use RockMigrations class loader to load all my custom pageClasses: https://github.com/BernhardBaumrock/RockMigrations/blob/2d85e460ce5aa394480906ad5b41cd1a0e86d0fe/RockMigrations.module.php#L1991-L2003 That might produce a little overhead, but I don't think that this get's noticable any time soon so the benefits outweigh by far.
  11. RF3 got a very nice little update today ? Aggregations Often we need to calculate sums or averages of table data quickly and efficiently. RockFinder3 makes that easy as well: $avg = $rockfinder->find("template=cat") ->addColumn('weight') ->getObject("SELECT AVG(weight)"); db($avg); $cats = $rockfinder ->find("template=cat") ->addColumns(['title', 'weight']); $cats->dump(); // dump finder data to tracy $obj = $cats->getObject("SELECT SUM(`weight`) AS `total`, COUNT(`id`) AS `cats`, SUM(`weight`)/COUNT(`id`) AS `avg`"); db($obj); // dump result of aggregation What happens behind the scenes is that RockFinder3 gets the current SQL query of the finder and adds that as FROM (...sql...) AS tmp to the query that you provide for aggregation. This is the resulting SQL query of the example above: SELECT SUM(`weight`) AS `total`, COUNT(`id`) AS `cats`, SUM(`weight`)/COUNT(`id`) AS `avg` FROM ( SELECT `pages`.`id` AS `id`, `_field_title_605cab16f38ce`.`data` AS `title`, `_field_weight_605cab16f3993`.`data` AS `weight` FROM `pages` LEFT JOIN `field_title` AS `_field_title_605cab16f38ce` ON `_field_title_605cab16f38ce`.`pages_id` = `pages`.`id` LEFT JOIN `field_weight` AS `_field_weight_605cab16f3993` ON `_field_weight_605cab16f3993`.`pages_id` = `pages`.`id` WHERE (pages.templates_id=44) AND (pages.status<1024) GROUP BY pages.id ) AS tmp You can even provide a suffix for your query to do things like GROUP BY etc: $rf = $rockfinder->find("template=cat|dog"); $rf->addColumns(['created']); $rf->dump(); db($rf->getObjects( "SELECT COUNT(id) AS `count`, DATE_FORMAT(created, '%Y-%m-%d') AS `date`", "GROUP BY DATE_FORMAT(created, '%Y-%m-%d')" ));
  12. I think I don't understand your question.. I don't have any functions in init.php nowadays. I have everything in init() of a module or in init() of a pageclass. If there is something in init() of a pageclass I have to trigger that manually, which is the reason for the loadClasses method of RM which triggers init() automatically https://github.com/BernhardBaumrock/RockMigrations/blob/2d85e460ce5aa394480906ad5b41cd1a0e86d0fe/RockMigrations.module.php#L1991-L2003 This is a simple pageClass I'm using in my CRM for managing contacts: <?php namespace RockCRM; use ProcessWire\HookEvent; use ProcessWire\Page; class Contact extends Page { const tags = RockCRM::tags; const tpl = RockCRM::prefix."contact"; const prefix = RockCRM::prefix."contact_"; const field_skills = self::prefix."skills"; public function __construct() { parent::__construct(); $this->parent = $this->wire->rockcrm->contacts(); $this->template = $this->wire->templates->get(self::tpl); } public function init() { $tpl = "template=".self::tpl; $this->wire->addHookAfter("ProcessPageEdit::buildForm", $this, "buildForm"); $this->wire->addHookAfter("Pages::saveReady($tpl,id=0)", $this, "onCreate"); } /** * Page edit screen */ public function buildForm(HookEvent $event) { $form = $event->return; $page = $event->process->getPage(); if(!$page instanceof self) return; $crm = $this->wire->rockcrm; /** @var RockCRM $crm */ $rm = $crm->rm(); // modify page edit screen of a contact if($f = $form->get('title')) { $f->label = 'Name of the Contact'; $f->notes = 'Enter a great Name'; } // or load scripts with the new update $rm->addScript(__DIR__."/Contact.js"); } /** * Migrate this pageclass */ public function migrate() { $crm = $this->wire->rockcrm; /** @var RockCRM $crm */ $rm = $crm->rm(); $rm->migrate([ 'fields' => [ self::field_skills => [ 'type' => 'text', 'tags' => self::tags, ], ], 'templates' => [ self::tpl => [ 'tags' => self::tags, 'icon' => 'user-o', 'noSettings' => 1, 'pageClass' => '\RockCRM\Contact', 'fields' => [ 'title', self::field_skills, ], ], ], ]); } /** * Things to do when a contact is created * @return void */ public function onCreate(HookEvent $event) { $page = $event->arguments(0); $page->status = 1; // published, non-temp $event->pages->names()->uniqueRandomPageName(); // unique name } } This setup is extremely powerful, extremely clear and extremely nice to work with ? Was that what you were asking for? ?
  13. The module gets better and better and I love working with it every day ? v0.0.42 has a nice little update to include scripts or styles in the PW backend easily. Why is that great? Because you can organize your files better and have everything in place while still having the ease of use (and you don't need to install any other modules for such simple tasks): Usage: I'm using custom page classes for EVERY page I have in my PW installs. One might think that this is overkill - but I'm wasting so much time on old projects looking for code snippets that are all over the project. Some in ready.php, some in init.php, another story are JavaScript snippets for small GUI tweaks... Not any more ? I have a custom PageClass "Foo" and there I have a method that modifies the page edit form (ProcessPageEdit::buildForm) - this is great for changing field properties like columnWidth or labels etc. on the fly without ever having to fire a migration. If I want to change something for my Foo pages, I head over to my IDE, type "Foo" and get directly to the Foo PageClass. But what about JavaScript? Today I needed a little snippet that populates some fields based on other fields. As easy as that: // in the buildForm hook $rm->addScript(__DIR__."/Foo.js"); Everything is well organized, because I have all my classes in a custom folder (inside a module, but the same would apply for the /site/classes folder): .../myModule/classes |-- Bar.php |-- Foo.js |-- Foo.php '-- Whatsoever.php And the JS file will only be loaded on the page edit screen to prevent overhead. Why not just use $config->scripts->add() you might ask? Good question ? $rm->addScript() will additionally add a cache busting timestamp to the file and it will only include the file if it exists on the system. It will also work on Windows systems, which you might forget to take into account if quickly adding it via $config->scripts->add() and last but not least it will also work on PW instances that are running in a subfolder ? if(!is_file($path)) return; $path = Paths::normalizeSeparators($path); $config = $this->wire->config; $url = str_replace($config->paths->root, $config->urls->root, $path); $m = $timestamp ? "?m=".filemtime($path) : ''; $this->wire->config->scripts->add($url.$m); Happy migrating ?
  14. Exactly - THX ? I should maybe use this panel more often, it looks very useful ?
  15. @adrian I think you mentioned that you are using some kind of sql query log often, but I can't remember where you were talking about this... any hints? ?
  16. thx, that works ? I've just created a PR to fix an issue with my field code panel ?
  17. Hi @adrian I know there are other cache related modules, but I didn't need any of those features over the last months/years. One thing that gets more and more annoying when developing backend apps is that the menu cache does not refresh when I refresh the modules cache. So for example I create a new module or do some changes, then I do a modules refresh via tracy and then I have to logout and login to see the changes in the pw backend navbar. Could tracys refresh trigger the menu cache to reset as well? Thx
  18. https://github.com/processwire/processwire-issues/issues/1356
  19. To be honest I've never really understood the maxage setting ? I tried to understand how it works again... but didn't. But I've found this working solution: $td = $files->tempDir('hello-world'); $td->setRemove(false); $td->removeExpiredDirs(dirname($td), 10); // remove dirs older than 10 seconds file_put_contents($td.'some-file.txt', 'Hello world'); Fire that from the tracy console several times and you'll see that on each request a new dir is created and also old dirs are removed. That's because you can't access files in a hidden folder! You'd need to write your own custom logic to include() the content of this file and show that on a custom url (maybe using the new url hooks). But the problem is that you have to keep track of the exact file/folder name of your temporary file. And another problem is, that you can end up with security problems, because the file content of a file can change over time whereas the link to that file could still be the one from an earlier version of another file. eg create temp file --> .../0/yourfile.txt create another --> .../1/yourfile.txt create another --> .../0/yourfile.txt (because the old file in folder 0 does not exist any more because it was too old and was removed) Now you got 2 links: yoursite.com/tempfile0 and yoursite.com/tempfile1 and tempfile0 will show different files depending on WHEN the link is clicked. I guess that's not a good idea ? I'll open an issue regarding the tempDir though. https://github.com/processwire/processwire-issues/issues/1356
  20. Wow, looks like you have put a lot of work into that ? I think every step towards better PW migrations is important and very welcome ? https://github.com/BernhardBaumrock/RockMigrations/blob/bb43552f55ef7ff57533083f4d886c3aa00a8e41/RockMigrations.module.php#L1956-L2000 $rm->migrate([ 'fields' => [...], 'templates' => [...], 'pages' => [...], ]); Maybe I'm missing something, but I thought I had that problem too when developing the migrate() method and it turned out to be quite easy: I create fields, then templates, then I setup the fields and templates again (now that fields and templates exist in the system the references can properly be set) and finally I create all pages. In my scenarios this has worked perfectly for several months (years?) now ? I have to think about that sentence ? Maybe you could elaborate a little more on that? I try to understand your workflows better. I always thought that if somebody does not want to learn how to use code-based migrations he/she could simply use PW's import/export tools?! Do you think you could create a quickstart-screencast using some free tool like https://screencast-o-matic.com/home to show the workflow when using your migrations?
  21. Hey Jens, could you please fix mistake (vor dem -Z-uschneiden) --> groß! Thx ?
  22. I'm quite sure it is possible, but I don't have an example. I'm not using RepeaterMatrix any more and on my old projects I did not use it in combination with RockMigrations, sorry ?
  23. Thx BitPoet for joining ? I have no idea what you are talking about - it's totally possible that I'm missing something obvious ?
  24. I'm getting this on every modules refresh and it gets a little annoying... Do you also experience this? Any ideas what the reason could be? That's the moduleinfo of the module...
×
×
  • Create New...