Leaderboard
Popular Content
Showing content with the highest reputation on 07/09/2024 in all areas
-
Custom Logs When you use the core $log->save() method you can only save a single string of text. When you view the log in the core ProcessLogger the columns and their header labels are predetermined. The Custom Logs module is different in that it lets you write and view log files with the number of columns and the column header labels you specify in the module configuration. Configuration In the "Custom logs" textarea field, enter custom logs, one per line, in the format... name: column label, column label, column label ...with as many comma-separated column labels as needed. The log name must be a word consisting of only [-._a-z0-9] and no extension. If you prefix a URL column label with {url} then the value in the column will be rendered as a link in the log viewer. The date/time will automatically be added as the first column so you do not need to specify it here. Writing to a custom log Use the CustomLogs::save($name, $data, $options) method to save data to a custom log file. $cl = $modules->get('CustomLogs'); $cl->save('my-log', $my_data); Arguments $name Name of log to save to (word consisting of only [-._a-z0-9] and no extension). $data An array of strings to save to the log. The number and order of items in the array should match the columns that are configured for the log. $options (optional) Options for FileLog::save(). Normally you won't need to use this argument. Example of use Custom log definition in the module configuration: visits: {url}URL, IP Address, User Agent, {url}Referrer Saving data to the log: $cl = $modules->get('CustomLogs'); $data = [ $_SERVER['REQUEST_URI'] ?? '', $_SERVER['REMOTE_ADDR'] ?? '', $_SERVER['HTTP_USER_AGENT'] ?? '', $_SERVER['HTTP_REFERER'] ?? '', ]; $cl->save('visits', $data); Viewing the resulting log in Setup > Logs > visits: https://github.com/Toutouwai/CustomLogs https://processwire.com/modules/custom-logs/7 points
-
What @da² mentioned above is kind of what I was thinking too, so I don't think he meant harm by it. You pointed out your comfort with HTML and confusion on multiline PHP, so the points are there for us to create an assumption. Apologies if it's wrong! ProcessWire is a content management framework, so it does require some level of understanding of development on both a higher and lower level, or at least the ability to read through code - it took me until just this year to realize that a lot of the documentation are actually in comments in the code, or in Markdown files that developers provide with their modules. I felt (partially) silly for only ever trying to look at written documentation. Like you, I love examples too; much of which exists in the forums, honestly. Anyhow, that being said, I may not be the best person to provide a response, but I do want to try to help you out here. The problem with these questions though are that each and every one can begin the response of the answer with: "It depends." Typically each template is not an entire HTML document, as you'll have one (or two) main layout file(s) that then get merged with your template in one way or another. This would depend on your output strategy as linked above. Since you're just starting out, it might make the most sense to give Direct output a try so that you can start to understand how ProcessWire works. I'm assuming though, since you read through the documentation, you'd also read the last part on the direct output page that mentions its drawbacks (I assume this based on question #3 above). Part of the solution to this, with direct output, is understanding the ProcessWire API. You can retrieve any "page" or page content within the system from pretty much anywhere by using the API. It's extremely powerful. Depending on your needs, you'd likely need to do some manipulation on the returned data prior to rendering it as HTML, but the point is that it's accessible to retrieve. This will depend on your own imagination on how you want to build your site profile out. If you've got some time, you might want to setup some test ProcessWire sites, and install some pre-made site profiles that you can examine and look through to understand how each developer decided how to set things up. There's no real right way to do things, and a lot of it will depend on the project's needs and requirements. Sure there are better ways to do things, but a lot of us don't figure that out until after we've made the not-better solution first. ? As a partial example to this question though - let's say you have a very simple blog. You have two system templates (thus far) - one that lists all blog posts, and one that views a specific blog entry (post). You have four template files: (1)header.php, (2)footer.php, (3)blog.php, (4)post.php. Posts are children of Blog, and Blog is the parent of Posts. (You could consider "Blog" as the homepage I guess in this very simple example.) Your header and footer contain your site's template, split up as expected. The blog.php and post.php files include the header and footer. (We won't worry about navigation at this point, this is a simple example.) The Blog template file uses the API to find and retrieve all Posts, or its children. It then outputs the title, as a hyperlink, to the blog post itself. (This could be adjusted to make sure entries are chronological - normally it would likely retrieve them that way, but depending on how you develop the system that might not always be the case.) if ($page->hasChildren()) { echo "<ul>"; foreach($page->children() as $post) { echo "<li><a href='$post->url'>$post->title</a></li>"; } echo "</ul>"; } The Post template file wouldn't need to know about any other pages; its purpose is to display the content contained for its own page. Surely it can though - it could display next, previous, or related posts. Again: The API can help with those scenarios (related would likely use tags or categorization of some sort). So when someone visits the post, as linked to from the Blog, the post.php file would use fairly simple PHP to output its own content. Let's assume the post system template has 3 fields: Title, Summary, and Body. The Blog excerpt above only shows how to loop through its child pages to output links, the Post excerpt below will have a bit more HTML just to hopefully provide more context. <h1><?= page()->title ?></h1> <div class="blog-summary"><?= page()->summary ?></div> <div class="blog-story"><?= page()->body ?></div> Remember, content above and below are handled by the header.php and footer.php files that are included in the post.php file, in this simple example. I could've added it above, but, well...oops. ? Are you asking about an HTML region, or a ProcessWire markup region? Verbiage with ProcessWire can get a little confusing, so I just want to be sure. Assuming you're just asking about some random area of HTML content on a page getting dynamically loaded from other areas of the system - you'd use the PW API. So let's say you want to get the title of the current page from the header.php file, which is a template file, not a (PW) system "template", so it doesn't have its own PW "page" fields, it's just a file. But you can still use the PW API to query all pages, get a resultset, and then process that data to output what you want. You've seen the children() method above. You can also use the pages()->get() method to retrieve the site's root homepage, which can be referred to either as "/", or 1...so that would be $homepage = pages()->get('/'); or $homepage = pages()->get(1);. From there, you can use the return value in $homepage and then find its children. You now have a first-level for your navigation links, if you decide to setup your navigation based on the tree structure of your site (you don't have to). I can't say whether this is any more helpful or not than other forum responses, or examples in the docs. Generalized questions are really hard to answer simply because there are so many ways to come up with a solution and a lot of it depends on many various factors.6 points
-
The PW forum is a place of the most friendly and helpful people I ever interacted with in web based CMS/CMF forums so far. What I learned from other forums is, that showing as much code you already tried (e.g. I put this code xxx into /site/templates/myfile.php) and asking a single question closely related to your code example (e.g. I expected to get x, but instead I see y) would help all involved people to better grasp each others standpoint and to provide better replies to specific questions. Cheers cwsoft3 points
-
The comment is valid. What you struggle with, is a basic using of the PHP language, not ProcessWire itself. That is also not a bad thing, I personally started learning PHP with ProcessWire. A PHP Tutorial that explains the basic things with examples close to easy HTML together can be a starting point. Or looking at the Code of the basic profile as you did and then research the things who are unclear with the PHP documentation. Gideons link is also a good starting point. Your last sentence is also not productive. There was no bad intention in the post before. This community here is an example of the opposite.3 points
-
I recommend checking out PW Tuts pwtuts.com. There, you can find basic concepts that will help you understand the fundamentals and allow you to create quite advanced websites.3 points
-
@joe_g, you could use a similar approach to that used by the Repeater Images module. Demo... Repeater Matrix field config: Hook in /site/ready.php: $wire->addHookBefore('Pages::saveReady', function(HookEvent $event) { /** @var Page $page */ $page = $event->arguments(0); if($page->template == 'demo_matrix_upload') { // File extension to Repeater Matrix type and subfield mapping $matrix_types = [ 'jpg' => [ 'type' => 'jpg', 'subfield' => 'image', ], 'mp4' => [ 'type' => 'mp4', 'subfield' => 'file', ], ]; // For each file in the "Upload files to Matrix" field foreach($page->upload_files_to_matrix as $pagefile) { // Skip file if it does not have an allowed file extension // It also makes sense to configure the upload field to only allow // the extensions you have matrix types for if(!isset($matrix_types[$pagefile->ext])) continue; // Add a Repeater Matrix item of the matching type for the file $item = $page->matrix_files->getNewItem(); $item->setMatrixType($matrix_types[$pagefile->ext]['type']); // Save the item so it is ready to accept files $item->save(); // Add the file $item->{$matrix_types[$pagefile->ext]['subfield']}->add($pagefile->filename); // Save the item again $item->save(); } // Remove all the uploaded files from the upload field $page->upload_files_to_matrix->removeAll(); } }); Result:3 points
-
But i need the heat for this cold days ^^ For real: Thx for the info and update notification!2 points
-
Why is it not constructive? Sorry that you interpret my comment that way. I've also read the output strategies tutorials and explanations were clear to me, so if you think "they don't make much sense" and you "struggle to read and comprehend the more complex code" I think you lack some PHP background. Maybe I'm wrong, it seems you are saying the problem is not about PHP knownledge, maybe you could explain so we can be more precise in the help we give to you.2 points
-
Really cool Robin! Just an FYI for Tracy users - I have pushed a new version which excludes any CustomLogs logs from the PW logs panel because they were just confusing with the incorrect headers and mismatched number of columns. Not sure yet, but I might build a new CustomLogs Panel to properly handle displaying them.2 points
-
Hi @Ade Welcome to the Processwire world. I started with the following tutorial. It help me a lot and I was no PHP developer at all at that time Gideon2 points
-
@Stefanowitsch is using FTP to deploy his websites and ran into problems with DDEV (see here for details). I didn't have these problems as I'm using automated deployments via RockMigrations + Github Actions, but I got this warning an every ddev start: Both issues are related as Randy explains here: https://github.com/ddev/ddev/issues/6381 The solution is to either disable mutagen or (imho better) to add this to your config: upload_dirs: - site/assets/files This should bring better performance, a startup without warnings and correct file permissions in /site/assets/files ?2 points
-
Page classes are an outstanding feature of ProcessWire and probably ranks among my favorites overall (and that's really saying something with ProcessWire). I don't remember how I lived without them, well I do, but I don't like to think about it. I wrote a response to a question asking how ProcessWire can help transition from procedural code to OOP and in the process (pardon the pun) of answering, I realized how much I've come to use, if not outright rely on, page classes in every project. I wanted to compile a few more thoughts and examples here because there may be devs who are finding this feature for the first time, and besides, why not go down the rabbit hole? Page classes have been around since ProcessWire 3.0.152 and if you're not familiar this will all make a lot more sense if you do a quick read of the feature announcement by Ryan here because this post assumes familiarity. We're also going to pick up the pace between the examples below compared to my comment in the link above. Here are a few goals that I have that page classes put solutions for within reach. You may have some of your own and I'd love to hear about those as well. Keep templates clean by restricting logic to flow operations- if statements and loops. Work with data in context, accessing data and fields closer to their source. Create "universal" methods available in every template, but also have scoped methods per-template Increase the scalability of a project, growth with limited increases in complexity. Stay DRY Embrace and extend the power of OOP in ProcessWire's DNA This seems like quite a list for one feature to handle, but rest assured, this is an example of how much power comes with using page classes. I'll continue and build on the blog example from my linked response above, and you can "follow along" with the Blog module since the templates mentioned are present out of the box. Some of the examples below are taken from production code, but please excuse any errors that I may have introduced by accident and I'm happy to update this post with corrections. Some of these things can be done other ways but they're just to illustrate, replace with your ideas and think of times that this would be useful. First up, what are some features and behaviors that everyone needs on every project? What are some universal methods that would be great to have everywhere? Let's start out by creating a "base" page class called DefaultPage. Going forward, page classes will extend this class instead of Page and benefit from having access to universal methods and properties. EDIT: I initially wrote this using BasePage rather than DefaultPage for this class name as described by the custom page class writeup by Ryan in the article above. I've changed this to now use the correct DefaultPage class name. <?php namespace ProcessWire; // /site/classes/DefaultPage.php class DefaultPage extends Page { /** * Returns all top level pages for the main navigation. */ public function navigationPages(): PageArray { $pages = wire('pages'); $selector = 'parent=1'; $excludedIds = $pages->get('template=website_settings') ->nav_main_excluded_pages ->explode('id'); // Check for excluded pages from nav defined in the "Website Settings" page count($excludedIds) && $selector .= ',id!=' . implode('|', $excludedIds); $homePage = $pages->get('id=1'); $topLevelPages = $pages->find($selector); $topLevelPages->prepend($homePage); return $topLevelPages; } } All your base page are belong to us. Right off the bat we've managed to pull complex logic usually in templates and kept our markup clean. This isn't for DRY, it's to store logic out of templates. This simple example only illustrates working with a top level page nav. We can start to appreciate the simplicity when considering how much more the navigation may call for in the future. A navigationChildren method that also accounts for excluded pages is a prime example. In our markup: <!-- /site/templates/components/site_nav.php --> <nav> <ul> <?php foreach ($page->navigationPages() as $navPage): ?> <li> <a href="<?= $navPage->url; ?>"><?= $navPage->title; ?></a> </li> <?php endforeach ?> </ul> </nav> Next up, our settings page has newsletter signup fields where users can copy/paste form embed code from Mailchimp. Our website has a "settings" page where global values and fields can be edited and maintained There are multiple textarea fields on the settings page that can each contain a different mailchimp embed code There is an embed select field that can be added to templates. The values are textarea field names so an embed can be chosen by the user. An embed select field has been added to the settings page which allows for choosing a default embed code if one isn't selected when editing a page Embedded forms should be available anywhere on any template <?php namespace ProcessWire; // /site/classes/DefaultPage.php class DefaultPage extends Page { // ...Other DefaultPage methods /** * Renders either a selected form embed, fallback to default selected form */ public function renderEmailSignup(): ?string { $settingsPage = wire('pages')->get('website_settings'); $embedField = $this->signup_embed_select ?: $settingsPage->default_email_embed; return $settingsPage->$embedField; } } Excellent. We've got some solid logic that otherwise wouldn't have a great home to live in without page classes. We can also modify this one method if there are additional options or complexity added to the admin pages in the future. Wherever we use renderEmailSignup, the return value will be either the selected or default form embed code. Let's create a folder called "components" in our templates directory that will hold standalone reusable markup we can use wherever needed, here's our newsletter signup component: <!-- /site/templates/components/newsletter_signup.php --> <div class="newsletter-signup"> <?= $page->renderEmailSignup(); ?> </div> Great! We've kept logic out of our code. We can render the field, and we can account for an empty value with a fallback. Unfortunately this is pretty limited in that it only handles a specific field, and it's implementation isn't as flexible as a component should be. We can fix that, and here are some new requirements to boot: Some templates may have multiple embed select fields which may or may not have a value Each embed select field is now paired up with a text field to add some content that appears with each form embed, think "Sign up for our newsletter today!" We want to render our mailchimp form embeds using the same component file, but handle different fields Sounds like a tall order, but it's a challenge easily overcome with page classes. We're going to introduce a new method called renderComponent that will be global, reusable, and very flexible. We're also going to make use of another very great ProcessWire feature to do it. Back to our DefaultPage class: <?php namespace ProcessWire; // /site/classes/DefaultPage.php class DefaultPage extends Page { // ...Other DefaultPage methods /** * Renders a file located in /site/templates/components/ with optional variables */ final public function renderComponent(string $file, ...$variables): string { $componentsPath = wire('config')->paths->templates . 'components/'; return wire('files')->render($file, $variables, ['defaultPath' => $componentsPath]); } /** * Renders either a selected form embed, fallback to default selected form */ public function renderEmailSignup(?string $embedSelectField = null): ?string { $settingsPage = wire('pages')->get('website_settings'); $embedField = $this->$embedSelectField ?: $settingsPage->default_email_embed; return $settingsPage->$embedField; } } We've done a couple of things here. We've updated renderEmailSignup to accept a field name, so now we're flexible on exactly which field select we'd like to check for a value on before falling back to default. We've also created a renderComponent method that is going to be super useful throughout the rest of our ProcessWire application. Our renderComponent receives a file name located in the components directory and any number of named parameters. You could change the $variables parameter to an array if you'd like, but I'm a big fan of the great features we have now in PHP 8+. Here's our refactored component file: <!-- /site/templates/components/newsletter_signup.php --> <?php if ($embedField): ?> <div class="newsletter-signup"> <?php if ($text): ?> <h2><?= $text; ?></h2> <?php endif ?> <?= $page->renderEmailSignup($embedField); ?> </div> <?php endif ?> And let's hop over to our (abbreviated) home page template: <body> <!-- ...Sections full of great content --> <section class="signup-call-to-action"> <?= $page->renderComponent('newsletter_signup.php', embedField: $page->embed_select, text: $page->text_1); ?> </section> <!-- ...Your awesome template design --> <section class="page-end-call-to-action"> <?= $page->renderComponent('newsletter_signup.php', embedField: $page->embed_select_2, text: $page->text_2); ?> </section> <!-- Your footer here --> </body> I don't know about you, but this is looking really good to me. The number of things we've accomplished while having written so little code is remarkable: Because we used wire('files')->render(), the entire ProcessWire API is available within the component, so now our renderEmailSignup method is too. The variadic function parameters (or array if preferred) let us pass an arbitrary number of variables to any component file, unrestricted future flexibility Variables are scoped to each component! There's no reference to template fields in our component that could break if changes are made No more PHP includes, we don't have to juggle paths or constantly repeat them in our code, nor rely on declaring variables before including a file. ProcessWire will throw an exception if we try to render a file that does not exist which makes locating issues very easy We'll also see an exception if we try to reference a variable in our component that wasn't passed which can also help troubleshooting. Notice that the renderComponent is final. We want that behavior to remain consistent everywhere we use it and not overwritten either intentionally or by accident on our inheriting page classes. We want to eliminate any confusion between templates by knowing it will always do the same thing the same way. We can explore other uses too, perhaps a renderPartial for files in /site/templates/partials where we store files like site_header.php. As mentioned above however, if a variable is expected in the rendered file but not included in our render method, we'll see an exception. Let's use site_header.php as an example because we're sure to run into situations where variables may or may not exist: <?php namespace ProcessWire; // /site/templates/partials/site_header.php ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?= $page->title; ?></title> <meta name="description" content="<?= $metaDescription ?? null; ?>"> <?php if ($includeAnalytics ?? true): ?> <script> // All that Google Analytics jazz </script> <?php endif ?> </head> <body class="<?= $bodyClasses ?? null; ?>"> <header> <?= $page->renderComponent('site_nav.php', includeEmailButton: true); ?> </header> Problem: solved. By using nullsafe ?? operators, we can call $page->renderPartial('site_header', description: 'The ultimate Spice Girls fan page.'); and never get errors for variables that may not be included when calling renderComponent, such as includeAnalytics, which now also has a default value of 'true'. Nice. We haven't even gotten to our actual page classes yet... Our templates are about to receive superpowers. Let's take our blog to the next level. In my comment on the other thread, I created a specific example of adding a readTime method to our blog posts, let's go one level higher to our blog.php template. We'll populate some methods up front and then talk about what we've done: <?php namespace ProcessWire; // /site/classes/BlogPage.php class BlogPage extends DefaultPage { /** * Get latest blog posts, optionally with/without pinned post, optionally in blog category */ public function latestPosts( int $limit = 3, bool $includePinnedPost = true, ?int $categoryId = null ): PageArray { $selector = 'template=blog-post'; if ($categoryId) { $selector .= ",blog_category={$categoryId}"; } $posts = wire('pages')->get($selector); if ($includePinnedPost) { $pinnedPost = $this->getPinnedPost(); if ($pinnedPost) { $posts->remove($pinnedPost); $posts->prepend($pinnedPost); } } return $posts->slice(0, $limit); } /** * Gets an optional pinned post if set/chosen * pin_blog_post A checkbox to indicate whether a post should be pinned * pinned_post A InputfieldPage field to choose a blog post */ public function getPinnedPost(): ?Page { if (!$this->pin_blog_post) { return null; } return wire('pages')->get($this->pinned_post); } } On our main blog page a user has the ability to choose whether a blog post is "pinned". A pinned post will always remain the first post anywhere a list of posts is needed, something like a big company announcement that the client wants to keep visible. These two methods alone have given us awesome abilities. For our main blog page, when someone visits our blog page, the most recent or pinned post is presented at the top, followed by the next two most recent posts, followed by two rows of 3 posts, for a total of 9. Let's assume that we've already created the BlogPostPage.php with the readTime method from my previous example. Here's our blog.php template <?php namespace ProcessWire; echo $page->renderPartial( 'site_header.php', bodyClasses: 'blog-page', metaDescription: $page->blog_description ); $posts = $page->latestPosts(9); $firstPost = $blogPosts->first(); ?> <section class="main-post"> <article> <img src="<?= $firstPost->blog_image->url; ?>" alt="<?= $firstPost->blog_image->description; ?>"> <h1><?= $firstPost->title; ?></h1> <?= $firstPost->summary; ?> <span><?= $firstPost->readTime(); ?></span> <a href="<?= $firstPost->url; ?>">Read More</a> </article> </section> <section class="recent-Posts"> <?= $page->renderComponent('blog_preview_card.php', blogPost: $posts->get(1)); ?> <?= $page->renderComponent('blog_preview_card.php', blogPost: $posts->get(2)); ?> </section> <section class="past-posts"> <?php foreach ($posts->slice(3, 6) as $post): ?> <?= $page->renderComponent('blog-preview_card.php', blogPost: $post); ?> <?php endforeach ?> </section> <?= $page->renderPartial('site_footer.php'); ?> So first off, we've really started to use our renderComponents method and component files. We also implemented a renderPartial as speculated upon above. Each does a similar thing, but having separate methods makes everything clear, handles paths, but still has a similar interface when calling them. A big thing to notice here is that at no point have we added any markup to our page classes, and no business logic to our templates. If we need to find anything, we know where to look just by glancing at the template. Ultimate maintainability. Here's our blog_preview_card.php component: <?php namespace ProcessWire; ?> <!-- /site/templates/components/blog_preview_card.php --> <article class="blog-preview-card card"> <img src="<?= $blogPost->blog_image->url; ?>" alt="<?= $blogPost->blog_image->description; ?>"> <h2><?= $blogPost->title; ?></h2> <div class="blog-summary"> <?= $blogPost->blog_summary; ?> </div> <span class="blog-read-time"><?= $blogPost->readTime(); ?></span> <a href="<?= $blogPost->url; ?>">Read More</a> </article> I am liking how well this is working out! Page classes have done a ton of heavy lifting here: We're using our renderComponent method to it's maximum potential and it's payed off in spades Our template couldn't be cleaner or more easily maintainable BlogPostPage.php has taken care of all of our needs as far as delivering the PageArray of posts and all our template does is output the data as needed Our "card" component will render the same thing everywhere and we can update how that looks globally with changes to one file If you don't think this can get more awesome, or think this post is already too long, I have bad news for you and you should stop reading now. Still here? Let's create a BlogPostPage class and add a method: <?php namespace ProcessWire; // /site/classes/BlogPostPage.php class BlogPostPage extends DefaultPage { // ... Other page methods, like readTime() public function relatedPosts(int $limit = 3): PageArray { return wire('pages')->get('template=blog')->latestPosts( limit: $limit, includePinnedPost: false, categoryId: $this->blog_category?->id ); } } The BlogPage::latestPosts method is really flexing it's muscle here. We've used it in two different places for different purposes but requesting similar data, blog posts. If you noticed, we're also specifying a category for this blog since we have a page select field that references a blog category. That was a parameter that we included back in our BlogPage::latestPosts() method. So, blog posts have a "You might also be interested in..." section with posts in the same category as the that one that a visitor has just read. With page classes this couldn't be easier to add, so let's use the relatedPosts method we just created in BlogPostPage.php in our blog-post.php template: <?php namespace ProcessWire; // /site/templates/blog-post.php echo $page->renderPartial( 'site_header.php', bodyClasses: 'blog-post', metaDescription: $sanitizer->truncate($page->blog_summary, 160) ); ?> <!-- The page hero, blog content, stuff, etc. --> <section class="related-posts"> <?php foreach ($page->relatedPosts() as $post): ?> <?= $page->renderComponent('blog_preview_card.php', blogPost: $post); ?> <?php endforeach ?> </section> <?= $page->renderPartial('site_footer.php'); ?> We've just added a related posts feature in *checks stopwatch* seconds. This project is going so fast that you can already hear the crack of the cold beer after a job well done. One more example, and I'll let you decide if this is too much, or feels just right. We want a blog feed on our home page. So before we go to the home.php template, let's do what we do (surprise, it's a page class): <?php namespace ProcessWire; // /site/classes/HomePage.php class HomePage extends DefaultPage { // Other HomePage methods... public function blogFeed(int $limit = 3): PageArray { return wire('pages')->get('template=blog')->latestPosts(limit: $limit); } } At first glance, this might seem like a bit much. Why create a method in our page class that is so short and simple? We could just call $pages->get('template=blog')->latestPosts(3) inside our template and get away with it just fine. Here's why I think it's worth creating a dedicated page class method: This creates yet another example of predictability between templates It promotes thie philosophy I like of page classes talking to page classes It's likely that we'll have other more complex methods in the HomePage class, keeping them together feels right and helps with uniformity- we always know where to look when seeing custom methods If we need to make a change to the BlogPage::latestPosts() method that affect how other page classes call that method, we don't have to root around in our templates to make any changes. It's pretty nice to see that thus far we have only ever referenced the $page object in our templates! There aren't any calls to $pages because our template is really about doing one thing- rendering the current page without caring about other pages. That's not to say that we can't, or shouldn't, use other ProcessWire objects in our templates, but it's still impressive how much we've been able to scope the data that we're working with in each template. I truly love the page classes feature in ProcessWire and even though this is a deeper dive than my other post, this really is still just scratching the surface because we can imagine having more complex behavior and other benefits. Here are some from my experience: Handling AJAX calls to the same page. Choosing how and what type of content is returned when the page is loaded is really nice. This can be done with hooks, but I really like page classes handling things like this in-context. Working with custom sessions between one or more pages. Creating a trait that shares session behavior between page classes is a great way to extend functionality while keeping it scoped. Adding external API calls and whatever complexity that may entail Provides the ability to add significant complexity to our templates in the admin yet little to no additional complexity in template files. The basic "pinned post" and form embed features are just the start As you can imagine, this makes replicating code you use often between projects trivial Page classes provide a home for logic that otherwise would be stuck in a template, or relegated to a messy functions.php file. No more functions.php ever. We met every single goal I listed here at the beginning Every single template thus far only uses if statements and loops DefaultPage could have a "bootApplication" method if needed that does things for every page and is called in ready.php. This is also a great way to create a "bootable" method in all of your page classes where bootApplication could call a bootPage method in your page classes if it exists. Thanks for coming to my TED talk. Hope you find this useful! If you have any questions, corrections, use cases, or things to add, the comments below are open and I'd love to hear them!1 point
-
Hi @Robin S - yes, it is easy to add custom panels to Tracy from your module, eg: https://github.com/wireframe-framework/Wireframe/tree/master/TracyPanels There isn't a way to hook into the Logs panel though at the moment, however, I think I have reasonable solution based on the new JSON viewing functionality I added recently in the latest version. Here is how that looks when viewing the logs panel with a custom visits log. Obviously not as nice as the PW logs viewer with the way you modified the table, but I think this works well enough and allows easy integration with the PW logs panel and its icon colour change to indicate new entries, combining with the latest from all logs, etc. Any thoughts before I commit?1 point
-
Now I am curious how you solved that. Can you post a snippet of your solution, please.1 point
-
So the next step is probably to learn PHP more. ?1 point
-
Wouldn't these two statements be contradictory? ? A redirection always change the current page. What you need is a link with target='_blank' like @Jan Romero wrote.1 point
-
Hi @Ade! I think your next step is elaborating on the output strategy! Have you taken a look at this section? https://processwire.com/docs/front-end/output/ I think it should answer all of your questions. An also popular and more complete/opinionated output solution is RockFrontend.1 point
-
Try following the steps here: https://www.perplexity.ai/search/first-steps-in-processwire-cms-vJ0lNwzDRY6eaXW.skftsw This should get you started.1 point
-
I'd be happy to provide something like that. I'm really short on free time right now but I may be able to adapt a real life example once I get enough done on one of my projects. That way I can avoid double work and have a good example. Will keep it in mind and bring that info back here as a demo repository when I can do it right. I've implemented the exact same method on sites before and that's a great example. I like to create a jsonld() method which returns null in my BasePage class that exists specifically to be overwritten by inheriting pages. That way I can call $page->jsonld() in a partial like site_header.php without any logic, it renders something or it doesn't.1 point
-
Hi, just a few words to say don't forget the .htaccess file at the site root that may need to be changed - to force with or without the www - probably to force https too - and, something if nearly always do in the htaccess, to add some headers, pw default ones in the htaccess are <IfModule mod_headers.c> # prevent site from being loaded in an iframe on another site # you will need to remove this one if you want to allow external iframes Header always append X-Frame-Options SAMEORIGIN # To prevent cross site scripting (IE8+ proprietary) Header set X-XSS-Protection "1; mode=block" # Optionally (O) prevent mime-based attacks via content sniffing (IE+Chrome) # Header set X-Content-Type-Options "nosniff" </IfModule> and i often end with something like that <IfModule mod_headers.c> # prevent site from being loaded in an iframe on another site # you will need to remove this one if you want to allow external iframes Header always append X-Frame-Options SAMEORIGIN # To prevent cross site scripting (IE8+ proprietary) Header set X-XSS-Protection "1; mode=block" # Optionally (O) prevent mime-based attacks via content sniffing (IE+Chrome) Header set X-Content-Type-Options "nosniff" Header set Content-Security-Policy "frame-ancestors 'self' https://www.helloasso.com" Header set Access-Control-Allow-Origin "*" Header set Referrer-Policy "no-referrer" Header set Permissions-Policy "geolocation=(), camera=(), microphone=()" Header always set Strict-Transport-Security "max-age=31536000; includeSubdomains" </IfModule> of course, just an exemple as helloasso is just here to say i allow iframes from this url (a french asso donation website) but you may need extra iframes, some of those can be more complex depending on your website needs more infos on those curious things here https://securityheaders.com/ ? have a nice day1 point
-
Lets see if we can get a quick-start tutorial going here. We'll start with something really simple and then work up from there. Tell me when something makes sense and when it doesn't and we'll adjust as we go. My thought is that we'd make a tutorial that plays on the 'hello world' phrase and lists information about planets in the solar system, starting with Earth. To keep it simple, we'll assume that the basic site profile is installed, as that's what comes with ProcessWire (so there's no need to uninstall anything). But we won't start using any of it's files tat this stage. Instead, we'll start out by creating our own files. STEP 1 – Create a template file Create a new file called: /site/templates/planet.php, and copy+paste the following HTML into that file: <html> <head> <title>Earth</title> </head> <body> <h1>Earth</h1> <h2>Type: Happy planet, Age: Millions of years</h2> <p>Earth (or the Earth) is the third planet from the Sun, and the densest and fifth-largest of the eight planets in the Solar System. It is also the largest of the Solar System's four terrestrial planets. It is sometimes referred to as the World, the Blue Planet, or by its Latin name, Terra.</p> </body> </html> The above is just a plain HTML file with nothing specific to ProcessWire. We will use this as the starting point for our template, and we'll go back and modify it later. STEP 2 – Add a template to ProcessWire Login to ProcessWire admin and go to Setup > Templates. This page shows a list of templates currently in the system. Click the Add New Template button. On the next screen that appears, you'll see it found your "planet" template file. Check the box next to the planet template and click Add Template. You may ignore any other options that appear on this screen. STEP 3 – Creating a page using your template Your planet template is now in the system and ready to use, but it's not being used by any pages. So lets create a page that uses the planet template. In the ProcessWire admin, click Pages in the top navigation. This is a site map if your page structure. We want to create a new page under the homepage, so click the new link that appears to the right of the home page. The next screen has 3 inputs: title, name and template. Enter "Earth" for the title, and the name should populate automatically. For the template, select planet. Then click Save. Now you have created a new page using the template that you added. You are now in the page edit screen and you should see your title field populated with "Earth". Click the View link that appears on this page edit screen. You should see the output of the HTML from step 1. Click the back button in your browser to return to the edit screen. STEP 4 – Creating a new field Now you know how to create a template and a page using that template. You could create more pages using the same template if you wanted to. But that wouldn't be particularly useful – this template file is just a static HTML file. Lets make it dynamic by creating some fields and adding them to it. We are going to create 3 fields to represent the pieces of data that currently appear in our static template. These include the planet's type, age in years, and a brief summary. We will call these fields: planet_type, planet_age and planet_summary. In ProcessWire admin, click Setup > Fields. This screen shows a list of fields currently in the system, most of which are general purpose fields for the basic profile. For the purposes of this tutorial, we are going to ignore those and create our own. Click the Add New Field button. On the next screen, enter "planet_type" for the Name, select "text" as the Type, and enter "Planet Type" for the Label. Then click the Save Field button. Now that your field is saved, you are on the Field Edit screen. At this point, your field is created and ready to be added to your planet template. Optional: While editing your field, click the details tab where you'll see a select box for Text Formatters. Select "HTML Entity Encoder" – this ensures that characters like "<", ">" and "&" will be converted to HTML entities and not confused as HTML tags. While not required, it's a good practice for text fields like this. After you've done that, click the Save Field button. STEP 5 – Creating more new fields In step 4 we created the planet_type field. Now we want to create the planet_age and planet_summary fields. So in this step, you'll want to do the same thing for the remaining two fields: Create the planet_age field exactly like you created the planet_type field, but enter "Planet age in years" for the label. Create the planet_summary field exactly like you created the planet_type field, but chose "textarea" as the Type and enter "Planet summary" for the label. Note that a "textarea" field is just like a "text" field, except that it can contain multiple lines of text. STEP 6 – Adding new fields to your template Now that you've created 3 new fields, you need to add them to your planet template. In ProcessWire admin, click Setup > Templates > planet. You are now editing your planet template. In the Fields select box, choose planet_type, then planet_age, then planet_summary. You will see each added to the list. Cick the Save Template button. STEP 7 – Editing a page using your template Now that you have new fields added to your template, go back and edit the Earth page you created earlier and populate the new fields that are on it. In ProcessWire admin, click Pages at the top, then click the Earth page, and click the edit button that appears to the right of it. You are now editing the Earth page you created earlier. You should see the new fields you added, waiting for text. Enter "Terrestrial planet" for Planet Type Enter "4.54 billion" for Planet Age in Years Paste in the text below for Planet Summary and then click Save. STEP 8 – Outputting dynamic data in your template file While still in the page editor from step 7, click the "View" link to see your page. Note that it still says "Happy planet" for type (rather than "Terrestrial planet") and "Millions of years" rather than "4.54 billion years". That's because the page is still being rendered with just the static data in it. We need to update the template file so that it recognizes the fields we added and outputs the values of those fields. Edit /site/templates/planet.php and replace the static text in there with tags like this, replacing field_name with the name of the field: <?php echo $page->field_name; ?> If supported by your server, you may also use this shorter format which some people find easier to look at and faster to enter: <?=$page->field_name?> Here is the /site/templates/planet.php file updated to output the content of the page using tags like the above: <html> <head> <title><?php echo $page->title; ?></title> </head> <body> <h1><?php echo $page->title; ?></h1> <h2>Type: <?php echo $page->planet_type; ?>, Age: <?php echo $page->planet_age; ?> years</h2> <p><?php echo $page->planet_summary; ?></p> </body> </html> After making these changes, save your planet.php template file. Now view your Earth page again. You should see it properly outputting all of the content you entered on the page, including "Terrestrial planet" for Type and "4.54 billion years" for age. Any changes you make from this point forward should be reflected in the output. STEP 9 – Creating more pages, reusing your template For this last step, we'll create another page (for Jupiter) using the same template just to demonstrate how a template may be reused. In ProcessWire Admin, click Pages and then click the new link to the right of the home page. Enter "Jupiter" as the Title and select "planet" for the Template. Click Save. Now that you are editing the Jupiter page, enter "Gas giant" for Type, enter "4.5 billion" for Age in Years, and copy+paste the following for Planet Summary: Click the Publish button and then View the page. You should now see your planet template being used to output the information for Jupiter rather than Earth. CONCLUSION In the above, we covered the basics of how to develop in ProcessWire, including the following: Creating templates and their associated template files Creating basic text fields and adding them to templates Creating and editing pages that use your templates Outputting the values of fields in template files If all of this makes sense so far, I thought we'd follow up next with a tutorial to take this further: Adding and outputting photos for each planet Creating navigation that lists all the other planets that have pages in the system …and we'd keep building upon the tutorial from there. If you all think this tutorial is helpful, then perhaps this can be a draft for a real tutorial we'll put on the site, so all of your help is appreciated in making this as good as it can be.1 point