Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 07/25/2022 in Posts

  1. I've written before about how to use Twig with ProcessWire (see my tutorials on integrating Twig into ProcessWire and extending Twig with custom functionality for ProcessWire). But those posts don't really talk about why I like to use Twig instead of plain PHP templates. For me, this comes down to one killer feature that I'm going to talk about below. But first, let's look at some of the more commonly mentioned advantages of Twig and why I don't actually think they're all that important in the context of ProcessWire: The syntax is nicer. While I personally agree with this, it's entirely subjective (and familiarity is comforting while trying something new is scary). Autoescaping provides security by default. This is true to a degree, but most ProcessWire projects (at least for me) aren't the type of expansive community-driven sites with lots of user-generated content where this would be most relevant. Most of my ProcessWire projects so far have featured a few trusted editors managing content, where you don't really need autoescaping for every template to make sure nobody slips in some malicious code. Twig forces separation of concerns between logic and presentation. Again, this is true, but not relevant to most ProcessWire projects. Most of my ProcessWire projects (and, judging by the showcase, most ProcessWire projects period) are mostly classic brochure sites without a lot of interactivity or app-like behaviour. Those projects are 99% presentation with only some small snippets of logic in between, so separating the two isn't really an issue. With that out of the way, let's talk about the killer feature that makes Twig essential to my work: Inheritance and block-based overwrites. To explain why this is important, I'll start out with a basic template for a header component in PHP and see how it can handle additional content being added to it. Then I'll write the same component in Twig for comparison. If you need a general guide on template inheritance in Twig, read this first: https://twig.symfony.com/doc/3.x/tags/extends.html The reusable header template Here's our basic reusable header template written in PHP: <header class="header"> <h1 class="header__headline"><?= $page->title ?></h1> <?php if ($page->subline): ?> <p class="header__subline"><?= $page->subline ?></p> <?php endif; ?> <?php if ($page->image) echo wireRenderFile('inc/responsive-image.php', ['image' => $page->image]) ?> </header> Sidenote, I'll use wireRenderFile to keep the examples brief, you could also write the image tag inline here. The header component may be included in a page template like this: <?= wireRenderFile('inc/header.php') ?> You have two options for where to do this. Option one is to include this template in every template that needs it (templates/home.php, templates/project.php, templates/news.php). Option two is to use the appendTemplateFile setting to keep the basic page layout in a shared template file that's always included at the end of the request (_main.php). Option one allows you to pass the template different variables depending on context, but it also means you've already started with the code duplication. Option two is probably the more common approach, but with this option you can only pass it one set of variables – those variables might be overwritten by the page template, but this will also lead to some problems as you'll see shortly. Let's introduce our first change request, one particular page needs to display a video instead of an image. No problem, we can just check if the page has a video field and display it conditionally: if ($page->video) { wireRenderFile('inc/video.php', ['video' => $page->video]); } elseif ($page->image) { wireRenderFile('inc/responsive-image.php', ['image' => $page->image]); } This still works fine. But crucially, the logic for the video header is now part of the header template, not part of the template for the page with video headers. This means that every time I want to edit the header template, this little piece of conditional logic is something I have to deal with. But that's fine, multiple pages might need a video header, so having this switch in the header template is acceptable. But then another change request come in: In the page template for some kind of project page, instead of the image, we want to display a list of project data coming from a separate project_data field. Again, we can adjust the template: if ($page->project_data) { wireRenderFile('inc/project-data.php', ['data' => $page->project_data]); } elseif ($page->video) { wireRenderFile('inc/video.php', ['video' => $page->video]); } elseif ($page->image) { wireRenderFile('inc/responsive-image.php', ['image' => $page->image]); } But now some display logic that's specific to one template is part of the global header template, not part of the project.php. This trend will continue: every custom feature required for the header of any page template will inflate the header.php file, and every adjustment requires reading all of it and making sure my change doesn't break any of the other features. This is unsustainable and inherently unscalable. Another example, what if a specific page has both the video and the image fields, but I want to display the image instead of the video? Currently, this is not possible. Now I have to build in some kind of switch: $preferImage = $preferImage ?? false; if ($page->project_data) { wireRenderFile('inc/project-data.php', ['data' => $page->project_data]); } elseif ($page->video && !$preferImage) { wireRenderFile('inc/video.php', ['video' => $page->video]); } elseif ($page->image) { wireRenderFile('inc/responsive-image.php', ['image' => $page->image]); } Again, this solution doesn't scale. Did you notice the subtle bug in there? The noise to signal ratio is becoming worse with every feature. Now you're probably thinking that you would approach those change requests in a different way. Let's look at some of the possible solutions to those problems. Lots of variables You can solve this to a degree by using lots of variables to control what you're template is doing. If we're using a shared _main.php template file that includes the inc/header.php template, the project-specific template (e.g. templates/project.php) is loaded first. So those templates can set some variables that change the content and behaviour of the header component. For example, say you want to do keep the template for the project data block in your project.php so it's easy to find. Let's go back to the original header template and introduce an optional variable that can be used to replace the image with something else: <?= $headerImageContent ?? wireRenderFile('inc/responsive-image.php', ['image' => $page->image]); Now you can set the $headerImageContent variable in your project.php and it will replace the image. But what if I want both the normal image (without duplicating code) AND some custom content? No problem, add even more variables: <?= $headerImageBeforeContent ?? '' ?> <?= $headerImageContent ?? wireRenderFile('inc/responsive-image.php', ['image' => $page->image]); <?= $headerImageAfterContent ?? '' ?> Now repeat that for every part of the header template which might need to be adjusted for some of the page templates (hint: it's all of them). You end up with a template that uses tons of variables, the signal to noise ratio becomes abhorrent. Throw in the fact that those variables are all unscoped, so there's no way to tell where they are being set or overwritten, and variable names have be very specific to avoid collisions. All of this might make sense to you the day you've written it, but what about your colleague that hasn't touched this project yet? What about yourself in six months? Make templates more granular Another solution is to make the templates more granular. I've started this trend above by using wireRenderFile to put little isolated template parts into their own dedicated template – for example, to display a single responsive image or an HTML5 video player. In the same grain, you can split up the header.php into multiple smaller template to mix and match and include include those you want to in each specific context. But this has downsides as well: You end up with a fractal nightmare, a deluge of templates with increasing granularity and decreasing utility, just to be able to include those smaller templates separately from each other. Cohesion and readability is reduced, and there's no way from directory structure alone to tell which templates go together in what ways. Splitting an existing template into two smaller templates is not backwards compatible – you have to make an adjustment in every place the original template was included. Or you keep the original template but change it to just include the two new templates. I said fractal nightmare already, didn't I? Duplicate code You can, of course, just keep separate header templates for each page type. But then you're duplicating the common parts of those templates all over again, and changing those means you have to touch a lot of separate files – definitely not DRY. Most real-life solutions will include a mix of the three approaches. I tried to be fair and write the templates in the leanest and cleanest way possible, but things still got out of hand quickly. Now let's look at the same component written in Twig: Resuable components in Twig Here's the basic header template but written in Twig: {# components/header.twig #} <header class="header"> <h1 class="header__headline">{{ page.title }}</h1> {% if page.subline %} <p class="header__subline">{{ page.subline }}</p> {% endif %} {% block header_image %} {% if page.image %} {{ include('components/responsive-image', { image: page.image, }) }} {% endif %} {% endblock %} </header> One important difference is the block tag defining the header_image block. So far we don't need that, but it will become important in a second. For the page templates, it's common to have a base template that all other templates inherit from: {# html.twig #} <!doctype html> <html lang="en" dir="ltr"> <head> <title>{%- block title -%}{%- endblock -%}</title> {% block seo %} {{ include('components/seo', with_context = false) }} {% endblock %} </head> <body> {% block header %} {{ include('components/header') }} {% endblock %} {% block content %}{% endblock %} {% block footer %} {{ include('components/footer') }} {% endblock %} </body> The base template defines some blocks and includes some default components (seo, header, footer). Now the template for the project page just inherits this: {# project.twig #} {% extends 'html' %} With the PHP template, things got difficult once we wanted to overwrite part of the header template with some content specific to one page template. This is where the header_image block comes in handy: {# project.twig #} {% extends 'html' %} {% block header %} {% embed "components/header" %} {% block header_image %} {# project data template … #} {% endblock %} {% endembed %} {% endblock %} Now the project.twig extends the base html.twig template and overwrites the header block. Then it includes the components/header template and overwrites only the header_image block while keeping the rest. This approach has some major advantages over the plain PHP template: All the code for the project template is in one place – to see what's special about this particular page in comparison to the base template, I just have to look at one template. I didn't have to repeat any of the header template code, so I can still change the header in a central place. The components/header template stays small and manageable, it doesn't know or care what other templates extend it and which parts get overwritten where. As a sidenote, some people may not like the embed syntax. Another approach would be to once again create a custom header template for the project template. But this time, we don't need to repeat any code because we can use inheritance: {# components/project-header.twig #} {% extends "components/header" %} {% block header_image %} {# project data template … #} {% endblock %} I prefer the embed approach because it keeps all the related code together. But both approaches allow for full flexibility with no code duplication. Now what if you want to change other parts of the components/header.twig template in an extending template? In this case, you can always add more blocks: {# components/header.twig #} {% block header_headline %} <h1 class="header__headline">{{ page.title }}</h1> {% endblock %} Adding blocks doesn't change anything about the base template, so it's 100% backwards-compatible. You can always add more blocks without ever having to worry about breaking any existing templates or introducing bugs. Another challenge for the PHP template was to add some additional content to a part of the header template while still keeping the default content. Let's say we want to display a publication date above the headline in a news template, but keep the headline as is. No problem: {# project.twig #} {% block header_headline %} <time>{{ entry.published_date }}<time> {{ parent() }} {% endblock %} The parent() function returns the content of the block in the base template, so you can extend a block without overwriting it completely. Conclusion You can solve all the challenges I posed here in PHP. Most solutions will include a combination of the three approaches mentioned above (making templates more granular, using lots of variables and duplicating code). And a well thought-out mix of those approaches can work reasonably well. The problem is that while those solutions improve reusability and scalability, they usually require lots of boilerplate code and unscoped variables. This reduces the readability and makes the system harder to modify, while making it easier for bugs to creep in. Again, there are solutions for those problems that introduce other problems until the solutions cancel each other out in trade-offs. To me, Twig is a great alternative that requires fewer trade-offs. It allows you to achieve complete freedom and flexibility in your templates all while keeping your templates DRY and keeping code that belongs together in a single file. On top of that, Twig uses a nice, readable syntax (warning: personal opinion) and provides a lot of utility methods and other features to improve your template structure. Some notable caveats to all of this: All of the discussed problems are about scaling a project to a larger scope or team size. For small projects that will never need to scale in this way, this doesn't really matter. ProcessWire's built-in markup regions seem to tackle a lot of the same problems I mentioned in this post. Can't really speak for it as I haven't tried it yet. If this all sounds interesting to you and you want to learn more, you can check out my tutorials on integrating Twig into ProcessWire and extending Twig with custom functionality for ProcessWire.
    6 points
  2. @Kiwi Chris @MarkE I think that probably could be achieved with a Fieldtype+Inputfield module pair. It sounds kind of like what FieldtypeCombo does except that you define the table structure in Combo, rather than Combo reading an existing table structure. No doubt it would be simpler than Combo since it would just read the table for its settings. Something like translating MySQL VARCHAR column to an InputfieldText, and a TEXT/MEDIUMTEXT column to a Textarea or CKE, and perhaps ENUM/SET columns to InputfieldRadios/InputfieldCheckboxes... that sort of thing. Could be fun to build!
    2 points
  3. Looking at your pseudo-code you're outputting a single image (the first one) from the page reference field, so you don't need to loop through it. // Get images of the author if they exist if $page->images !='': foreach $page->images as $source: echo $source->size(100,100)->url; endforeach; // Otherwise get images of their book covers else: $source = $page->page_ref_for_their_books->images->first; echo $source->size(100,100)->url; endif;
    1 point
  4. Somewhere in your template where you expect the variables to appear try the following code: print_r($_GET); // or echo $_SERVER["QUERY_STRING"]; What do they output? Also you might want to take a look at this module PaymentPaypal (PaymentPaypal) - ProcessWire Module
    1 point
  5. The ProFields Combo field and Table fields create SQL tables behind the scenes, but they're still fieldtypes, so are expected to be added to a ProcessWire template and are linked to the page tree. Sometimes that's not necessary or desirable. With regards to creating custom tables, I'm quite happy writing SQL statements for this, or using the generated CREATE TABLE statements with a database export from mySQL, or for that matter even other dialects of SQL, with a bit of editing if necessary. It's not hard to write a CREATE TABLE statement initially, then an ALTER TABLE statement if subsequent changes are needed. I think this would play nice with your RockMigrations module to generate SQL schema changes in code, and I don't want to turn ProcessWire into mySQLAdmin or Adminer which is installed with Tracy Debugger, but it would be nice to have an end user friendly UI to view and edit custom SQL tables in the ProcessWire admin. You have made me thing of something I hadn't considered, about how to migrate SQL listers and editing forms, if the InputField settings or fields to display in a lister change, but if they're accessible via the API, it should be possible to manage migrations in a similar way to ProcessWire native fields and templates.
    1 point
  6. @ryanFieldtypeCombo comes pretty close, but like you say, you define the table structure with the fieldtype. Also as a fieldtype it needs to be associated with a template, that links into the ProcessWire page tree, which isn't always desirable with SQL tables from an existing database. I'm thinking there would probably need to be two configurable Process modules. An ProcessSQLLister and ProcessSQLForm module. Names are just suggestions ProcessSQLTemplate could be another option however as it's not an output template, it's an admin UI for editing arbitrary SQL data. ProcessSQLLister would be similar to Lister Pro, except for selecting a source, you'd select an SQL table, then choose what fields from the table to display in the lister. Dealing with lookup fields which are similar to page reference fields, but can refer to an arbitrary database table would probably be the biggest challenge, but the basic lister should be pretty straightforward. Edit functionality would depend on a related ProcessSQLForm being defined for the specified table. ProcessSQLForm would involve picking an SQL table, then selecting fields from the table to include on the form in the order desired, and mapping and configuring a sane InputField type depending on the SQL field type. @kixe has already made FieldtypeSelectExtOption which can handle lookups from arbitrary SQL tables, so that InputField should be able to handle relational reference fields, and core InputFields should be able to map to other SQL fields types. For both, access control at the table level would be desirable. While access control at the field level would be technically possible, I don't think it's necessary at least initially, especially if it adds complication since the fields are SQL fields rather than ProcessWire fields. I can try to mock something up, but if someone else beats me to it, then feel free to go ahead. I think ProcessWire has all the nuts and bolts to do this, it's just a case of putting them together.
    1 point
  7. RockFrontend will have a render() method that can render latte files. In my setups the main markup file is PHP and there I just use something like this: <head> <?= $rockfrontend->styles('head') ->add('my/theme.less') ->add('foo/bar.css') ->render() ?> </head> <body> <?= $rockfrontend->render('sections/header.latte') ?> <?= $rockfrontend->render('sections/main.latte') ?> <?= $rockfrontend->render('sections/footer.latte') ?> </body> Then I have empty PHP files for each template so that PW knows that these templates are viewable on the frontend. Ugly, but that's how PW works. Latte docs show how you can render latte files here: https://latte.nette.org/en/develop
    1 point
  8. Module allow you to load events from event files. Visit Github page or module directory for usage
    1 point
  9. There weren't very many CMS/CMF platforms either. I started out with SSIs to start reducing redundancy and duplication, then ended up starting my own CMS around 2000, but I picked a loser in terms of development language, and I realised trying to maintain a CMS for a modest number of clients on my own wasn't really efficient use of time. Someone introduced me to ProcessWire in 2015, and since then nearly all my websites have been converted to it. Web development and database development have progressed together for me, so ProcessWire's separation of content and presentation, and strong access control are critical for me. I evaluated my other CMS platforms before I settled on ProcessWire and I simply didn't like them as they either had a steep learning curve or were too output focused or both. I wouldn't use Word to try to build a relational database, nor would I use WordPress. I think if there is one major thing still on my wish list, it would be the ability to have a UI using ProcessWire inputfields to create data entry forms for arbitrary SQL tables. A common scenario for me is converting offline databases (often Microsoft Access) to cloud based alternatives. Usually it's possible to restructure everything within ProcessWire's pages model, but sometimes I just want to be able to dump the data across and build a UI quickly with no need for data to exist within a page model. I've been playing around a bit looking at building module(s). I'd love the goodness of Lister/(Pro), but instead of picking a template, pick an arbitrary SQL table and specify fields from it, set access control etc in the same way Lister Pro woks, and then have I guess what I'd call an SQLTemplate where you'd pick an arbitrary SQL table, select fields from it, and for each one specify an inputfield, within the contraints of the SQL field type. Of course none of this data would be accessible within ProcessWire's page structure out of the box, however there's already $database->query() to execute an arbitrary SQL query and return a result set, so using arbitrary SQL data within templates is already easy, so the only bit missing is a UI to manage arbitrary SQL tables as easily as ProcessWire pages in admin, but it's already halfway there with inputfields that don't care where their data comes from.
    1 point
  10. Introducing Padloper Addons Addons allow you to easily extend the functionality of your Padloper Shop. All you need is to drop your addon files (.php) in /site/templates/padloper/addons/. There are currently two broad types of addons: Payment Addons and 'Custom Addons'. Addons only need to implement the PadloperAddons Interface. Payment addons must also extend the abstract PadloperPayment Class. That's it. With addons, your shop admins can perform various tasks without leaving the context of the shop. Your addons have access to the powerful ProcessWire and Padloper APIs. You can build anything you want. You are only limited by your imagination, or your budget, should you want me to develop a custom addon for you ?. Documentation is in the works, ready sometime this week. Once this is done, I'll release the latest Padloper that includes this feature. Below, a video demo of a few addons I developed to showcase this new feature. Please note that the addons demoed in the video are not part of the official Padloper distribution. Can't wait to see what you guys will build?.
    1 point
  11. Good news! We are live now! AppApi has been approved and now appears in the modules directory: https://modules.processwire.com/modules/app-api/ Thank you for your many reactions to the release - I hope it helps you build the best apis you can imagine!
    1 point
×
×
  • Create New...