Overview

Finally, I have a little time for start migration from WordPress to Processwire. I’m waited so long. Latest version of this site I made from template, because I didn’t have enough time to make all from scratch. I know, it was a mistake, but I deal with it. Truly, I had though about changing the back-end engine even before, experimented with MODx. But MODx was to slow, just like a wordpress. But I liked the conception of CMF. And after some time, I had found ProcessWire. It works like a MODx, even better, and it’s extremely fast, really. I had fell in love! And I don’t want render pages on the server, not this time. And again, ProcessWire brilliant solution here. In this article I will mark some aspects of developing new M.O.Z.G Store, as first point of further migration to this engine for the whole project. Truly, for whole life I had worked with many CMS/CMF: Joomla, WordPress, MODx, Bitrix, Drupal and various ugly CMS developed by my employers. And I absolutely sure that PW beats most of them! Why I think so? Because no more barriers between you and your idea. If you are not a professional web-developer you don't have thousand hours of practice to learn others' ideas, just for a work. ProcessWire gives you most flexible API, so you can create your own workflow, as like as you want. And it can't be easily expanded when you need. CMS shouldn't put conditions on every step, and dictate how you work!

Some introducing

I have lived with WordPress for a long time, very long as I think. Always did not have time to create something new from scratch. Especially because I not PHP guru, and know nothing about back-end programming. But I’m well versed in the technical design, thanks to my old jobs. And it is not helped with wordpress. I show pros/cons below, so you will understand better:

WordPress

Pros

Working just out-of-the-box.

Not required any skills and time for render default content.

Hundreds of addons for any task.

Cons

To change basic core behavior need tons of time for read Codex and override dozen of core functions.

Not common API. Each plug-ins works as it wants, with different interface, scripts, styles, API, database structure, etc. You have to read manuals for each of them, even if you buy it for $199.

It’s impossible to remake interface and core functional for yourself without rewriting dozens of bloody damned built-in functions.

Painful upgrades. Always something demands to be renewed. Core send you notification every day. You site bothered you. Why should it be so? Every plugin have its own logic, and will be overwritten after each update. You should either learn code of each and override it in function.php (but not every addon allows it) or change something just in the code of plugin. And track every update. Mandess! Madness? This is Wordpreeeeees!

Conclusion

Really, WordPress made for pain. It’s simply to use, if you want nothing to do with design, layout and basic behavior. But if you want to do something more - welcome to painvalley! Any your task it’s new battle against WordPress’ core!

ProcessWire

Pros

Brilliant FTP (fields/templates/pages) conception, which permeates the whole system

You are creator, literally. Useful JS engine allows you to create any interface for pages. And main conception make management very powerful.

You already know how it works - It works like you want!

Handy and simple API.

The modules (local plug-ins) don‘t create new essences, and perfectly integrated!

Simple and clear file system and MySQL structure.

Great admin, ideally for Developers, Designer and Clients. It is really rare among CMS!

Core and Site are separated. You don't need change nothing in file structure of core. Every config, template, and modules downloading and works only inside site folder. It simplifies migration and upgrades.

Small but strong and active Community of Developers, not hamsters.

It’s extremely fast!

Cons

You must have intermediate programming skill just to render something more than text. But API is very simple. PW have a few paid modules and front-end formatters, but you just forget about server-side rendering during work. Really, why I should think about built-in rendering, if I can render everything as I want. After all, receiving a pure data has never been so easy! (I mean CMS/CMF only, PHP framework allows it of course, but they don't have so handy admin).

You must to design everything. But you can do it by your own way. Result will be more clear than with any other CMS.

Pages have a very wide scope, sometimes it may to confuse. Formally, its' not "pages" in the sense we use to. Just forget all what yo know about traditional pages, plugins, CMS, etc.

Conclusion

ProcessWire it’s perfect solution for designers. Great flexibility allows you to create and expand your project. It is still present routine operations that can take time, but only in the early stages of development. But you don’t need to read kilometers of manual, you will immediately start working.

Powerful interface engines and Fields conception allows to create and expand awful interfaces in admin panel. Without extra strings of PHP code, great, isn’t it?

I will add some examples of my cases during developing. Interesting to me, and I think to some one too. You should to see how it’s amazing after WP.

More than just pages. More than just strings.

One of most important parts of PW philosophy is pages. Opposite to most popular CMS, pages is more than just addresses, somewhere on the site. It's global variables, lists, data containers, configs, etc. That's why philosophy of PW may to confuse you at start of usage. But this feeling will leave you after a few days, believe me. Ryan Cramer, developer and evangelist of PW, compares this behavior with file system of an application. Each program contain many files, but end-user sees and works only with several. On the other hand, URLs may not be strictly attached to pages. For example, when you use URL segments, you may create totally virtual structure of site, and use pages as datablocks. I can get any data from existent or virtual location and use its by my own way. Personally, I like to consider ProcessWire as handy MySQL manager, just interface for organize. Ideally, if you can't or don't want to write your own back-end by yourself.

For example, I have the data structure of file formats in my store. If I want get all supported file formats in my output or field's list. It may be reached very easy with selector "has_parent=/product-meta/formats/, parent!=/product-meta/formats/, include=hidden". "has_parent" selector looks through the all children, and sub-children, and sub-children, etc. But our sub-categories it's just a pages too. That's we use "parent" selector with "noequal" operator to exclude sub-categories. All this pages is hidden and doesn't have physical template file. So user can't reach this via URL or Search request.

Selectors is other most powerful feature of the API that allows you get any data with a simple textstring. It's working everywhere, from admin and API requests. You may use IDs, names, templates, parents, field values, values of page fields inside another fields (like Repeater Field), etc. All your data is always related with each other. In ProcessWire 3 selectors have become even better, and supports selectors array inside selector.

And what if I tell you that users, admin panels and everything else are pages/templates/fields too? Yes, you can associate any data with user, and get it just like a page (with permissions of course). Even sections in admin panel is just a pages with its own templates. Do you feel what prospects it opens?

Credits fields

Most commonly I work on my projects alone, but sometime I work with concept artists or developers. That’s why I decided make credits fields for products and portfolio items. It’s simply:

Create two new templates, call them “basic-title” and “staff-member”. They will be universal templates, strict relation not required. “basic-title” contains only one field “title”. And “staff-memeber” contains “title” and “link” fields. And, may be, “bio” and “image” in future, who knows?

Then create pages: Credits, Credits->Roles, Credits->Names. Assign “basic-title” template with Roles’ child-pages, and “staf-member” with Names’ child-pages.

Now most interesting part of this task. Install Repeater module, then add new Repeater field, call it “product_cast”.

Add new Page field, call it “product_cast_role”, go to Input tab and change:

“Parent of selectable page” to our Roles page

“Template of selectable page” to “basic-title”

“Label field” to “title (default)”

“Input field” type to “Select”

And check “Allow new pages to be created from field”

Well, now clone this field, call it “product_cast_name”. Use Staff page and “staff-member” template here, as we done it on the steps 1, 2. Now create new Repeater field, call it “product_cast”, go to Details tab, and add our new fields.

Assign “product_cast” field with any page. Great, we have good interface now.

Now, will try to render our data. I will use PW for JSON output to webapp, and I don’t need to render HTML structure. But you will understand all from next example.

Open template file of our page assigned with “product_cast” field, and write something like this:

if($input->urlSegment1 === 'json') { header("Content-type: application/json"); $arrayProductMeta = array(); ... $arrayCast = $page->product_cast; $i = 0; foreach ($arrayCast as $member) { $arrayProductMeta['product_cast']['member' . $i]['role'] = $member->product_cast_role->first()->title; $arrayProductMeta['product_cast']['member' . $i]['name'] = $member->product_cast_name->first()->title; $arrayProductMeta['product_cast']['member' . $i]['url'] = $member->product_cast_name->first()->link; $i++; } $output = json_encode($arrayProductMeta, JSON_UNESCAPED_UNICODE); echo $output; exit(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if ( $input -> urlSegment1 === 'json' ) { header ( "Content-type: application/json" ) ; $arrayProductMeta = array ( ) ; . . . $arrayCast = $page -> product_cast ; $i = 0 ; foreach ( $arrayCast as $member ) { $arrayProductMeta [ 'product_cast' ] [ 'member' . $i ] [ 'role' ] = $member -> product_cast_role -> first ( ) -> title ; $arrayProductMeta [ 'product_cast' ] [ 'member' . $i ] [ 'name' ] = $member -> product_cast_name -> first ( ) -> title ; $arrayProductMeta [ 'product_cast' ] [ 'member' . $i ] [ 'url' ] = $member -> product_cast_name -> first ( ) -> link ; $i ++ ; } $output = json_encode ( $arrayProductMeta , JSON_UNESCAPED_UNICODE ) ; echo $output ; exit ( ) ; }

And as result, our JSON:

{ "product_cast":{ "member0":{ "role":"Designer", "name":"Alexander "M.O.Z.G" Dikov", "url":"http:\/\/facebook.com\/alxander.dikov" }, "member1":{ "role":"Concept Artist", "name":"Ekaterina "Lindi Laiquende" Jabina", "url":"" } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "product_cast" : { "member0" : { "role" : "Designer" , "name" : "Alexander " M . O . Z . G " Dikov" , "url" : "http:\/\/facebook.com\/alxander.dikov" } , "member1" : { "role" : "Concept Artist" , "name" : "Ekaterina " Lindi Laiquende " Jabina" , "url" : "" } } }

So you can change, or expand data for each item. In most cases you may prefer to use Options FieldType, but it’s good only for static, unexpandaple data, eg.: Yes/No, CS6/CC/CC2014, etc. Options Fieldtype creates and keep data in DB, and very sensitive to changes. And it not allow you dynamically add/change new data just from page editor. Moreover, don't use Options Fieldtype when some content may require expansion in the future. Maybe that's why Options FeildType is disabled by default.

Repeater FieldsType it's interesting, I think you should to understand how it works. When you add another fields inside Repeater, it create new system template for each Repeater field. You can see it on the Templates page as tagged by System templates.

When you create page with repeater, and fill data to it, PW create new page with unique ID. And when you request repeater data from page via API, you will get just a PageArray. It sounds to difficult, but it's works greatly. Infact, Repeater it's just automated interface to create page structures. You anyway need something like this in your project. So why write it from scratch!?)

You see it, we have just received page data array from two categories, just with the regular PHP loop, without any tricky query requests, complex functions or something else. It’s just a pure PHP, you already know what to do. And all variables you know, because you had created them by yourself. You can even override any system field, title for example. It’s PW folks, it’s good!

Getting data

Getting Fieldsets

ProcessWire have nothing except Pages/Templates/Fields, all what you see is subject of them, and Fieldset not exception. It's just one open tag and auto-generated close tag with "_END" suffix. That's why you can't get fields from fieldset, API returns this tags like any other field. I show small function below that allows you to get only fields from fieldset.

function extractFieldSet($n) { $start = false; $result = array(); foreach (wire( 'page' )->template->fields as $field) { if ($field->name == $n) { $start = true; } elseif ($field->name == $n . "_END") { break; } elseif ($start) { $result []= $field; } } return $result; } foreach(extractFieldSet("YOUR_FIELDSET_NAME") as $item) { echo $item->name . "

"; //Print name of each field inside YOUR_FIELDSET_NAME fieldset. } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function extractFieldSet ( $n ) { $start = false ; $result = array ( ) ; foreach ( wire ( 'page' ) -> template -> fields as $field ) { if ( $field -> name == $n ) { $start = true ; } elseif ( $field -> name == $n . "_END" ) { break ; } elseif ( $start ) { $result [ ] = $field ; } } return $result ; } foreach ( extractFieldSet ( "YOUR_FIELDSET_NAME" ) as $item ) { echo $item -> name . "

" ; //Print name of each field inside YOUR_FIELDSET_NAME fieldset. }

It's may be useful when you need to output many similar data, only from a fieldset, but not from whole page.

Filtered and processed output

Well, let's write some code to get our data like we want. Before I show the code, I have to tell a little about my logic. Besides the usual fields such the title, body, etc., I have so-called Product Meta fields. These fields will be rendered as table data. For example: size, resolutions, textures included and etc. I want ability to expand or reduce this data, without necessary to code the same behavior for such fields. Some developers told me that never use such behavior in their projects. May be it's a bit exotic, but I don't think so.

$fieldsArray['meta'] = array(); // Create new array for our meta fields. $ignoreFieldNames = array('product_grafx_width', 'product_grafx_height'); // Field names that I want to be ignored. $ignoreFieldTypes = array('FieldtypeFieldsetOpen', 'FieldtypeFieldsetClose', 'FieldtypeFieldsetTabOpen', 'FieldtypeFieldsetTabClose'); // Field types that I want to be ignored. foreach(extractFieldSet('product_meta') as $item) { // Extract fields from Fieldset by Function described above. $name = $item->name; $type = $item->type->name; $pType = $page->product_type; $fieldObject = $page->$name; // Return field as Object not a Data Array $isShown = ( $page->is($item->showIf) || $item->showIf == null ) ? true : false; // Status of visibility based on condition logic. By the way, it's just a selector string, as I told before! if ( $isShown && !in_array( $type, $ignoreFieldTypes ) && !in_array( $name, $ignoreFieldNames ) ) { $fieldsArray['meta'][$name]['title'] = $fields->getLabel($name); // Getting multi-lingual label for each field. This is custom function, I'll tell about later. if ( $type == 'FieldtypePage' ) {// Processing for Page Fieldtype below if($item->inputfield == 'InputfieldCheckboxes') { // Check if InputfieldType is Checkboxes. unset( $fieldsArray['meta'][$name] ); // Delete name of Options field from main array, we'll keep only its Pages. $fieldsArray['meta'] = extractOptions( $item, $fieldsArray['meta'] ); // Custom function pushes generated array to $fieldsArray['meta'], for each available Page in this Field as "Title, Value:Yes|No". } else if ( get_class($fieldObject) == 'PageArray') { // If FieldObject class is PageArray (ProcessWire/PageArray for PW3) format it as string of values. $i = 0; $length = count($fieldObject); $fieldsArray['meta'][$name]['value'] = ''; foreach ($fieldObject as $item) { if ( $length > 1) { $separator = ($i == $length - 1) ? '.' : ', '; } else { $separator = ''; } $fieldsArray['meta'][$name]['value'] .= $item->title . $separator; $i++; } } else { // For simple Page Selector, returns just a title of selected Page. $fieldsArray['meta'][$name]['value'] = $fieldObject->title; } // } elseif ( ... ) { // Any your custom conditions. } else { $fieldsArray['meta'][$name]['value'] = $fieldObject; // Returns default formatted Output for a Field. } } echo json_encode($fieldsArray, JSON_UNESCAPED_UNICODE); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 $fieldsArray [ 'meta' ] = array ( ) ; // Create new array for our meta fields. $ignoreFieldNames = array ( 'product_grafx_width' , 'product_grafx_height' ) ; // Field names that I want to be ignored. $ignoreFieldTypes = array ( 'FieldtypeFieldsetOpen' , 'FieldtypeFieldsetClose' , 'FieldtypeFieldsetTabOpen' , 'FieldtypeFieldsetTabClose' ) ; // Field types that I want to be ignored. foreach ( extractFieldSet ( 'product_meta' ) as $item ) { // Extract fields from Fieldset by Function described above. $name = $item -> name ; $type = $item -> type -> name ; $pType = $page -> product_type ; $fieldObject = $page -> $name ; // Return field as Object not a Data Array $isShown = ( $page -> is ( $item -> showIf ) || $item -> showIf == null ) ? true : false ; // Status of visibility based on condition logic. By the way, it's just a selector string, as I told before! if ( $isShown && ! in_array ( $type , $ignoreFieldTypes ) && ! in_array ( $name , $ignoreFieldNames ) ) { $fieldsArray [ 'meta' ] [ $name ] [ 'title' ] = $fields -> getLabel ( $name ) ; // Getting multi-lingual label for each field. This is custom function, I'll tell about later. if ( $type == 'FieldtypePage' ) { // Processing for Page Fieldtype below if ( $item -> inputfield == 'InputfieldCheckboxes' ) { // Check if InputfieldType is Checkboxes. unset ( $fieldsArray [ 'meta' ] [ $name ] ) ; // Delete name of Options field from main array, we'll keep only its Pages. $fieldsArray [ 'meta' ] = extractOptions ( $item , $fieldsArray [ 'meta' ] ) ; // Custom function pushes generated array to $fieldsArray['meta'], for each available Page in this Field as "Title, Value:Yes|No". } else if ( get_class ( $fieldObject ) == 'PageArray' ) { // If FieldObject class is PageArray (ProcessWire/PageArray for PW3) format it as string of values. $i = 0 ; $length = count ( $fieldObject ) ; $fieldsArray [ 'meta' ] [ $name ] [ 'value' ] = '' ; foreach ( $fieldObject as $item ) { if ( $length > 1 ) { $separator = ( $i == $length - 1 ) ? '.' : ', ' ; } else { $separator = '' ; } $fieldsArray [ 'meta' ] [ $name ] [ 'value' ] . = $item -> title . $separator ; $i ++ ; } } else { // For simple Page Selector, returns just a title of selected Page. $fieldsArray [ 'meta' ] [ $name ] [ 'value' ] = $fieldObject -> title ; } // } elseif ( ... ) { // Any your custom conditions. } else { $fieldsArray [ 'meta' ] [ $name ] [ 'value' ] = $fieldObject ; // Returns default formatted Output for a Field. } } echo json_encode ( $fieldsArray , JSON_UNESCAPED_UNICODE ) ;

And our output may looks like:

{ "meta" : { "product_ae_resizable" : { "title" : "Resizable", "value" : "No" }, "product_ae_plugins" : { "title" : "Plugins", "value" : "Yes" }, "product_ae_ue" : { "title" : "Universal Expressions", "value" : "No" }, "product_grafx_resolution" : { // 'grafx' because I use one Resolution Field for multiple product types. "title" : "Resolution", "value" : "720x576" }, "product_video_anaformat" : { // This is auto-calculated field. It's interesting and important, I'll tell about it later. "title" : "Anamorphic Format", "value" : "1050x576" }, "product_video_par" : { "title" : "Aspect Ratio", "value" : 1.46 }, "product_AE_plugins" : { "title" : "Plugins", "value" : "Element 3D v1, Trapcode Particular." } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 { "meta" : { "product_ae_resizable" : { "title" : "Resizable" , "value" : "No" } , "product_ae_plugins" : { "title" : "Plugins" , "value" : "Yes" } , "product_ae_ue" : { "title" : "Universal Expressions" , "value" : "No" } , "product_grafx_resolution" : { // 'grafx' because I use one Resolution Field for multiple product types. "title" : "Resolution" , "value" : "720x576" } , "product_video_anaformat" : { // This is auto-calculated field. It's interesting and important, I'll tell about it later. "title" : "Anamorphic Format" , "value" : "1050x576" } , "product_video_par" : { "title" : "Aspect Ratio" , "value" : 1.46 } , "product_AE_plugins" : { "title" : "Plugins" , "value" : "Element 3D v1, Trapcode Particular." } } }

extractOptions

As you had saw above, I had used custom function for Checkboxes, now I tell how it works, and way. By default ProcessWire returns PageArray only for selected pages, but in my case I need all Pages that available for the Field.

// $f variable get our Field item, and $array - whole Meta array generated for this moment. Then our new merged array returns back to the page template. function extractOptions($f, $array) { $i = 0; $name = $f->name; $booleanItems = array(); $tempArray = array(); foreach (wire( 'page' )->get($name) as $option) { // Get all selected Pages, and make array. $booleanItems []= $option->name; } $boolParent = $f->parent_id; // Get parent Page's ID for Options field. $bools = wire( 'pages' )->get( 'id=' . $boolParent )->children( 'include=all' ); // Get all pages from parent. foreach ($bools as $bool) { $boolName = $bool->name; $tempArray[$boolName]['title'] = $bool->title; if ( in_array( $bool->name, $booleanItems ) ) { // Looks if Page from parent is selected inside the field. $tempArray[$boolName]['value'] = __('Yes'); } else { $tempArray[$boolName]['value'] = __('No'); } } return array_merge( $array, $tempArray); // Merge main Meta array with our Options array and returns it back to the Page's template. } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // $f variable get our Field item, and $array - whole Meta array generated for this moment. Then our new merged array returns back to the page template. function extractOptions ( $f , $array ) { $i = 0 ; $name = $f -> name ; $booleanItems = array ( ) ; $tempArray = array ( ) ; foreach ( wire ( 'page' ) -> get ( $name ) as $option ) { // Get all selected Pages, and make array. $booleanItems [ ] = $option -> name ; } $boolParent = $f -> parent_id ; // Get parent Page's ID for Options field. $bools = wire ( 'pages' ) -> get ( 'id=' . $boolParent ) -> children ( 'include=all' ) ; // Get all pages from parent. foreach ( $bools as $bool ) { $boolName = $bool -> name ; $tempArray [ $boolName ] [ 'title' ] = $bool -> title ; if ( in_array ( $bool -> name , $booleanItems ) ) { // Looks if Page from parent is selected inside the field. $tempArray [ $boolName ] [ 'value' ] = __ ( 'Yes' ) ; } else { $tempArray [ $boolName ] [ 'value' ] = __ ( 'No' ) ; } } return array_merge ( $array , $tempArray ) ; // Merge main Meta array with our Options array and returns it back to the Page's template. }

Hidden Fields and Edit Pages via API

I haven't words to say how easily such procedure with PorcessWire. I'll show you better with example:

$page->of(false); // 'of' is short form of old '<span class="descr">setOutputFormatting'</span> method. <span class="descr">This turns off output formatting, which ensures that saved values don't already have runtime formatters applied to them.</span> if ( $page->is('product_type!=1076') ) { // If product isn't 3D Model. $width = $page->product_grafx_width; //Get width from field. $height = $page->product_grafx_height; //Get height from field. $page->product_grafx_resolution = $width . 'x' . $height; //Write new value to hidden field. } $page->save(); //Save the page. $page->of(true); //Switch Output Formatting to normal. 1 2 3 4 5 6 7 8 $page -> of ( false ) ; // 'of' is short form of old '<span class="descr">setOutputFormatting'</span> method. <span class="descr">This turns off output formatting, which ensures that saved values don't already have runtime formatters applied to them.</span> if ( $page -> is ( 'product_type!=1076' ) ) { // If product isn't 3D Model. $width = $page -> product_grafx_width ; //Get width from field. $height = $page -> product_grafx_height ; //Get height from field. $page -> product_grafx_resolution = $width . 'x' . $height ; //Write new value to hidden field. } $page -> save ( ) ; //Save the page. $page -> of ( true ) ; //Switch Output Formatting to normal.

Maybe, someone will ask me why I use multiple Fields here, if I could create TextField and write simple string "WWWxHHH". Yes, I could, but I use Width and Height in multiple functions, and I need this value as Integers. It does matter - write a small function here, or several parsing in various functions. Or I could create a variable for simple output, but I want be able to rename, reorder and get it from loop, that's why I had use just the hidden field for this task.

Infact, I have to write Module to run such code afterSave, but no time now, it will be next task after all. API is very good, and you can construct modules with settings very easily. And each of them (except Ryan's paid modules) is open-sourced, you can just modify existent module, and look how it works. Community is very strong and good.

As you can see, it's very interesting, and easy. You can write your own query function, and use it any way you want. API allows to do amazing things.

Musical pause;)

Custom Multilingual output

Here we come to one of the interesting topics - Multi-language support in ProcessWire. Since PW2.2, LanguageSupport module is included to the core, it's work great, but have its own nuances. By default PW respects only User's Language, and URL prefixes for Pages, and only for Pages. All of this is good, but in my case I want another behavior. I wanna get all translated data (if available) based on language of the Page, but not user. OK, purpose is clear, open your _init.php file and write:

$GLOBALS['$pageLang'] = _x('en', 'Language code'); // Create gloabl variable with contextual, translatable string 'value'. $GLOBALS['$pageLocale'] = _x('en_US', 'Locale code'); $GLOBALS['$pageCharset'] = _x('UTF-8', 'Charset'); //Locale settings setlocale(LC_ALL, $GLOBALS['$pageLocale'] . '.' . $GLOBALS['$pageCharset']); // Set PHP local to Page's language. 1 2 3 4 5 6 $GLOBALS [ '$pageLang' ] = _x ( 'en' , 'Language code' ) ; // Create gloabl variable with contextual, translatable string 'value'. $GLOBALS [ '$pageLocale' ] = _x ( 'en_US' , 'Locale code' ) ; $GLOBALS [ '$pageCharset' ] = _x ( 'UTF-8' , 'Charset' ) ; //Locale settings setlocale ( LC_ALL , $GLOBALS [ '$pageLocale' ] . '.' . $GLOBALS [ '$pageCharset' ] ) ; // Set PHP local to Page's language.

Then go to language settings (Admin >> Setup >> Languages >> NAME_OF_LANGUAGE), press "Translate File" buttons, press "Refresh File List" if _init.php translations is not exist.

Well, now we will get translated global variables for regional pages, and built-in PHP functions, such as date, will be translated. But labels still rendered with default language. To fix it behavior, let's write small hook in my _func.php:

$fields->addHook('getLanguageLabel', null, 'getLangLabel'); function getLangLabel($event) { $field = $event->arguments(0); $lang = wire('user')->language->isDefault() ? '' : wire('languages')->get($GLOBALS['$pageLang'])->id; $event->return = wire('fields')->get($field)->get("label$lang"); // Pay attention to the quotes, inline variables works only within double quotes. } 1 2 3 4 5 6 7 8 $fields -> addHook ( 'getLanguageLabel' , null , 'getLangLabel' ) ; function getLangLabel ( $event ) { $field = $event -> arguments ( 0 ) ; $lang = wire ( 'user' ) -> language -> isDefault ( ) ? '' : wire ( 'languages' ) -> get ( $GLOBALS [ '$pageLang' ] ) -> id ; $event -> return = wire ( 'fields' ) -> get ( $field ) -> get ( "label$lang" ) ; // Pay attention to the quotes, inline variables works only within double quotes. }

Now, if Page's language isn't default, we will get name from "labelLANGUAGE_ID" field's parametr instead default "label" column. It happens due the compatibility matters - Fields doesn't keep their multilingual names in separate columns unlike the Pages. ProcessWire is very conservative in the matters of MySQL compatibility . That's why Ryan doesn't change working behavior for MySQL dramatically. MySQL database is the same even between major version of PW. By the way, it's very handy, each upgrade doesn't brake your data, you can switch between versions without extra risks.

Finally, NetBeans, finally

By the way, during preparation for the work, I found that NetBeans now (8.1 at the moment) finally looks like good IDE. Many years ago I try to use it for a work, but always it seemed terrible. Really, ugly, terrible design, strange behavior. But, IDEA now available only with subscription, and such business model not fit to my requirements. But I think that IDEA it's perfect solution anyway, just I can't use it for my purposes. If you, like me, are under the impression of IDEA, it's good news for you. You can download Darcula LAF theme for NetBeans, it's totally cloned IDEA's Dracula theme. Now your NetBeans IDE feels and looks like IDEA) Moreover, since v8.1, NetBeans supports Gulp and Grunt! I like Gulp, and now like NetBeans too. I doesn't feel frustration from latest versions anymore.

Debugging is never been so easy before

ProcessWire, as some other CMS has Tracy debugger module. It's great framework for PHP debugging, and even better in the incarnation for ProcessWire. Adrian Jones make great work, and updates his module often. For example, you may have multiple dev templates versions, and quickly switch between them. Even between core versions. Great CMF and great debugger together, what can be better, isn't it!? Full feature list is available on the official page.

Now I take a break, and I'm going to prepare front-end environment. I'll be back as soon as begin to connect it with the back-end, stay tuned!

Why JS Framework?

Why I need JavaScript framework for front-end? I'm freelancer, design and support my own project by myself, and really doesn't want to render something on the server, then attach js libraries, learn all new and new APIs, styles, guides, etc for each of them. For example, I was made big conditional form in the past. In the beginning it has planned as simple server-rendered form, ok. But after few months I had decided to add interactivity to this form, and hell broke loose! Usage jQuery and extra libs seems fast and easy in the beginning, but it's terrible.

I chose Riot.js 2 after all, because it's totally satisfy my requirements. I haven't any experience with MV* frameworks before, and I can't imagine why I should use Angular or Backbone. It seems to me that developers of js frameworks do what they do with Mobile App on dedicated node.js server in mind. All these full-stacked frameworks with tricky API, many unique essences inside themselves, ugly syntax. May be it's good, I don't know. If you reads any article about MV* frameworks, you may see dozens of people, who talks a lot about architecture, draws diagrams, and compares the performance. But the final argument - technology determine by specifics of the application. WTF!? In the end, to understand the tool is right for my purpose, I have to start use it, but to use it, you need a lot of days to read large manuals of each. I had read about Angualar then about Ember.js, Knockout.JS, but I just spent time wasted, those tools not fit to my purposes.

After many weeks of investigation of this subject I found Vue.js, and almost start use it, but have encountered with problems. First, Vue.js recently have updated from 0.12 version to 1.0, and something (really, I haven't read whole guide about) start working different. I was failed to run some examples with new version of Vue.js. Ok, I was ready to deal with it, but why my JSON data doesn't parsed through Vue, I couldn't understand. Well, what's next? On a forum in the Vue.js subject, I found references to Riot.js, as more unobtrusive framework then Vue. And I was decided to try...

So why Riot?

I had very simple requirements for JS framework:

Simple syntax

Small and clear API (Yes, after ProcessWire I want handy API everywhere))

Ability to easily change the design

Ability to code and structure as I want

Ability to compile pre-rendered DOM. It's good for SEO, I can render header's meta on the server and then work with this tags with JS framwork.

Routing

Orientation to web-sites and NOT node.js backend. Or just without specified orientation

Ability to create my own components and expand functional

Well, I don't want much more, but many MV* frameworks can't give it to me. But Riot do! I had read manual in one evening, and already in next morning made working prototype of product page, Bingo! But why? Because Riot use existing technology, and nothing more. Every web developer already have HTML, CSS to represent view. We don't must to reinvent wheel and construct view for page, if we already have HTML, and know how it work. It's very similar to approach of ProcessWire, nothing extra there. Moreover, you can write your tags (local components) in HTML or external files, compile on client side, on the server or via package builder (for example Gulp). Each approach has strong and weak sides, but you have a choice! Compiler it's a greater one. If in others MV* frameworks you have to write constructors with pure JavaScript, here you can do it too, but normal approach is using HTML+JS+CSS syntax. But now it's time to shut up and show you code:

product-page h3 {data.title} h5 {data.released} #product-meta ul li(each='{name, subArray in meta}', class='{name}', no-reorder) span { subArray.title } span { subArray.value } script(type="coffee"). console.log 'Product is: ' + opts.data.title @data = opts.data @meta = opts.data.meta @on 'updated', -> console.log 'Product page updated!' @on 'update', -> console.log 'Product page start update event' @on 'unmount', -> console.log 'Product page is unmounted now!' style(scoped, type="stylus"). #product-meta ul li span width 50% text-align right display inline-block 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 product - page h3 { data . title } h5 { data . released } #product-meta ul li ( each = '{name, subArray in meta}' , class = '{name}' , no - reorder ) span { subArray . title } span { subArray . value } script ( type = "coffee" ) . console . log 'Product is: ' + opts . data . title @ data = opts . data @ meta = opts . data . meta @ on 'updated' , -> console . log 'Product page updated!' @ on 'update' , -> console . log 'Product page start update event' @ on 'unmount' , -> console . log 'Product page is unmounted now!' style ( scoped , type = "stylus" ) . #product-meta ul li span width 50 % text - align right display inline - block

Code above it's my product-page tag coded with Jade, Coffeescript and Stylus. It's very simple example, but in some cases your tag may contain only DOM tags and nothing more. Obviously why I use Jade (remark, project now renamed to Pug, but Pug name will be allowed only in Riot 3 version), and coffeescript just because I don't like long loops and curly braces in JS. Stylus is amazing style lang, in my past works I always used SCSS and Bourbon framework, but later switch to Kouto-Swiss for Stylus. Riot 2 supports next pre-precessors:

html

jade (deprecated)

pug

css

less

sass

scss

stylus

* Only less is available on browsers.

js

none or javascript

livescript

typescript

es6 - (using babel-core or babel)

babel - (using babel-core v6.x and the es2015 preset)

coffee or coffeescript

So, you can to code with almost any favorite pre-processors, it make tags very flexible, and you can reuse each part in other your products.

A few words about coffee

Some programmers perhaps will tell me that Coffeescript is dead. But think about it. Coffeescript, Typescript, EcmaScript 6 (with babel to support all browsers) it's just compiled to pure EcmaScript 5 languages. Of course, use of the latest achievements make habit, but where it can be useful for? And it's not matter of just browsers. Sometime I develop scripts for Adobe Photoshop, Illustrator and After Effects, and ExtandScript is just EcmaScript with taste of Adobe. Some clients use oldest version of Adobe software, where I can't just implement libraries to my code. Yes, Photoshop CS4 parse JSON only through eval() or has errors. Therefore, Babel, CoffeeScript, TypeScript, LiveScript and etc., it's just a syntax sugar, and nothing more. After some years, in hypothetical future rules of the game can be anything. 10 years ago, the use of JavaScript was considered bad taste, and tried to avoid this, everything changed. But in fact, I don't have Python-like syntax alternative, except Coffeescript, at the moment, but I very like this feature.

Components

Riot.js have components library for rapid prototyping also. If you like SCSS, you will love it, but if not, you may just use RiotGear components as base for your own components. For example easy to rewrite tags to support some Stylus frameworks, like Yeti.css, for example.

Write your own module

As I promised you previously, now we make own module for generate values for hidden fields. API is great! I knew nothing about modules in ProcessWire, but made this one for a hour! Easy task, easy implementation! So must be!

/** * First part of Classname it's name of group also. For example, my module will be placed * as subitem in "Mozgstudio" group. * I use ConfigurableModule insted Module, because want to add some configuration in admin. */ class MozgstudioProductMataGenerator extends Process implements ConfigurableModule { // Populate the default config data. Now it's just a variable. protected static $defaults = array( 'supportedTemplates' => 'product-page' ); // getModuleInfo is a module required by all modules to tell ProcessWire about them. public static function getModuleInfo() { return array( 'title' => 'Product Mata Generator', 'version' => 6, // Return 0.0.6 'summary' => 'This module generate values for hidden Product Meta\'s Fields.', 'author' => 'Alexander "M.O.Z.G" Dikov', 'href' => 'https://mozg-studio.org', 'singular' => true, // Only one instance of this module allowed 'autoload' => true, 'permanent' => false, // Module can be uninstalled. 'icon' => 'shopping-cart', 'autoload' => "template=admin", 'requires' => 'ProcessWire>=2.5' ); } public function init() { // Run function before Save action. $this->pages->addHookBefore('save', $this, 'generateHiddenFieldsData'); } public function generateHiddenFieldsData($event) { $page = $event->arguments('page'); // Return page object. /** * Code below is equivalent to normal API requests, like in your templates. * $this->supportedTemplates is variables that we'll define in the next * static function. It's controllable from admin. */ if ( in_array( $page->template->name, $this->supportedTemplates ) ) { if ( $page->is('product_type!=1076') ) { // Yes, I had changed my mind :) $res = explode('x', $page->product_grafx_resolution); $page->product_grafx_width = $res[0]; $page->product_grafx_height = $res[1]; // Report changes to admin notification bar. $this->message("Resolution has changed to " . "{$page->product_grafx_width}" . "x{$page->product_grafx_height}."); } switch ( $page->product_type->id ) { case '1075': $dpi = $page->product_grafx_dpi; $page->product_grafx_dimension = round($res[0] / $dpi, 3) . 'x' . round($res[1] / $dpi, 3) . '"'; // Report changes to admin notification bar. $this->message("New print demension is {$page->product_grafx_dimension}."); break; case '1077': $page->product_video_anaformat = $this->calcResolutionByPAR( $res[0], $res[1], $page->product_video_par); // Report changes to admin notification bar. $this->message("New anamorphic format is {$page->product_video_anaformat}."); } if ( $page->pub_date && $page->published !== $page->pub_date ) { $sqlPubDateUpdateQuery = 'UPDATE pages SET published="' . date('Y-m-d H:i:s', $page->pub_date) . '" WHERE id=' . $page->id .''; wire('db')->query($sqlPubDateUpdateQuery); // Report changes to admin notification bar. $this->message("Pub-date '" . strftime('%d %b %Y', $page->published) . "' was overwritten with current Release date '" . strftime('%d %b %Y', $page->pub_date) . "'." ); } } } // Custom function to calculate anamorphic resolution. public static function calcResolutionByPAR( $width, $height, $par ) { if ( $par > 1 ) { $width = round( $width * $par ); } elseif ($par < 1) { $height = round( $height * $par ); } return $width . "x" . $height; } public static function getModuleConfigInputfields(array $data) { $data = array_merge(self::$defaults, $data); $fieldSupportedTemplates = wire('modules')->get('InputfieldAsmSelect'); $fieldSupportedTemplates->attr('name+id', 'supportedTemplates'); $fieldSupportedTemplates->label = __('Supported templates', __FILE__); $fieldSupportedTemplates->columnWidth = 100; $fieldSupportedTemplates->description = __("Product Meta Generator will" . "process only selected templates. Leave blank to allow all templates.", __FILE__); $fieldSupportedTemplates->setAsmSelectOption('sortable', false); foreach(wire('templates') as $t) { // Add template as option only if Template's flag not equal System (integer value). if(!($t->flags & Template::flagSystem)) $fieldSupportedTemplates->addOption($t->name); } if(isset($data['supportedTemplates'])) $fieldSupportedTemplates->value = $data['supportedTemplates']; return fieldSupportedTemplates; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 /** * First part of Classname it's name of group also. For example, my module will be placed * as subitem in "Mozgstudio" group. * I use ConfigurableModule insted Module, because want to add some configuration in admin. */ class MozgstudioProductMataGenerator extends Process implements ConfigurableModule { // Populate the default config data. Now it's just a variable. protected static $defaults = array ( 'supportedTemplates' = > 'product-page' ) ; // getModuleInfo is a module required by all modules to tell ProcessWire about them. public static function getModuleInfo ( ) { return array ( 'title' = > 'Product Mata Generator' , 'version' = > 6 , // Return 0.0.6 'summary' = > 'This module generate values for hidden Product Meta\'s Fields.' , 'author' = > 'Alexander "M.O.Z.G" Dikov' , 'href' = > 'https://mozg-studio.org' , 'singular' = > true , // Only one instance of this module allowed 'autoload' = > true , 'permanent' = > false , // Module can be uninstalled. 'icon' = > 'shopping-cart' , 'autoload' = > "template=admin" , 'requires' = > 'ProcessWire>=2.5' ) ; } public function init ( ) { // Run function before Save action. $this -> pages -> addHookBefore ( 'save' , $this , 'generateHiddenFieldsData' ) ; } public function generateHiddenFieldsData ( $event ) { $page = $event -> arguments ( 'page' ) ; // Return page object. /** * Code below is equivalent to normal API requests, like in your templates. * $this->supportedTemplates is variables that we'll define in the next * static function. It's controllable from admin. */ if ( in_array ( $page -> template -> name , $this -> supportedTemplates ) ) { if ( $page -> is ( 'product_type!=1076' ) ) { // Yes, I had changed my mind :) $res = explode ( 'x' , $page -> product_grafx_resolution ) ; $page -> product_grafx_width = $res [ 0 ] ; $page -> product_grafx_height = $res [ 1 ] ; // Report changes to admin notification bar. $this -> message ( "Resolution has changed to " . "{$page->product_grafx_width}" . "x{$page->product_grafx_height}." ) ; } switch ( $page -> product_type -> id ) { case '1075' : $dpi = $page -> product_grafx_dpi ; $page -> product_grafx_dimension = round ( $res [ 0 ] / $dpi , 3 ) . 'x' . round ( $res [ 1 ] / $dpi , 3 ) . '"' ; // Report changes to admin notification bar. $this -> message ( "New print demension is {$page->product_grafx_dimension}." ) ; break ; case '1077' : $page -> product_video_anaformat = $this -> calcResolutionByPAR ( $res [ 0 ] , $res [ 1 ] , $page -> product_video_par ) ; // Report changes to admin notification bar. $this -> message ( "New anamorphic format is {$page->product_video_anaformat}." ) ; } if ( $page -> pub_date && $page -> published !== $page -> pub_date ) { $sqlPubDateUpdateQuery = 'UPDATE pages SET published="' . date ( 'Y-m-d H:i:s' , $page -> pub_date ) . '" WHERE id=' . $page -> id . '' ; wire ( 'db' ) -> query ( $sqlPubDateUpdateQuery ) ; // Report changes to admin notification bar. $this -> message ( "Pub-date '" . strftime ( '%d %b %Y' , $page -> published ) . "' was overwritten with current Release date '" . strftime ( '%d %b %Y' , $page -> pub_date ) . "'." ) ; } } } // Custom function to calculate anamorphic resolution. public static function calcResolutionByPAR ( $width , $height , $par ) { if ( $par > 1 ) { $width = round ( $width * $par ) ; } elseif ( $par < 1 ) { $height = round ( $height * $par ) ; } return $width . "x" . $height ; } public static function getModuleConfigInputfields ( array $data ) { $data = array_merge ( self :: $defaults , $data ) ; $fieldSupportedTemplates = wire ( 'modules' ) -> get ( 'InputfieldAsmSelect' ) ; $fieldSupportedTemplates -> attr ( 'name+id' , 'supportedTemplates' ) ; $fieldSupportedTemplates -> label = __ ( 'Supported templates' , __FILE__ ) ; $fieldSupportedTemplates -> columnWidth = 100 ; $fieldSupportedTemplates -> description = __ ( "Product Meta Generator will" . "process only selected templates. Leave blank to allow all templates." , __FILE__ ) ; $fieldSupportedTemplates -> setAsmSelectOption ( 'sortable' , false ) ; foreach ( wire ( 'templates' ) as $t ) { // Add template as option only if Template's flag not equal System (integer value). if ( ! ( $t -> flags & Template:: flagSystem ) ) $fieldSupportedTemplates -> addOption ( $t -> name ) ; } if ( isset ( $data [ 'supportedTemplates' ] ) ) $fieldSupportedTemplates -> value = $data [ 'supportedTemplates' ] ; return fieldSupportedTemplates ; } }

As you can see, nothing difficult here. Of course, if you planning to write big complex modules, you should read API reference, but for small simple tasks it's not required. After WordPress, usage one simple API in front-end and Back-end feels like a bloody magic! Now I have to write some features in the beta site, but I don't know. I can't just open WP code after PW, blood drips from my eyes when WP)

Musical pause;)