Jump to content
Gadgetto

[SOLVED] Check if a field has a unique value across all pages (page editor)

Recommended Posts

Hi there,

I'd like to prevent duplicate values in a specific page field (FieldtypeText) across all pages. The hook should also prevent saving of the page if a duplicate value is detected. Therefore I created two hook methods but I can't get it to work properly.

Here are both hooks placed in init() method of an autoloader module:

The first hook is to initially preset the field with the page id. The field value can be customized by the admin before saving.
The second hook should check if the value is unique.

If I create a new page and try to save, the WireException is already triggered in first step of page creation, where you enter the page title: SKU [] is already in use

If I check the value of $sku (with Tracy) it's always empty!

Any idea what could be wrong?

EDIT: if I remove the second hook, the field value is correctly preset with the page id!

$this->addHookAfter('Pages::added', $this, 'presetProductFields', ['priority' => 99]);
$this->addHookAfter('Pages::saveReady', $this, 'checkSKUUnique', ['priority' => 101]);

And this are the hook methods:

public function presetProductFields(HookEvent $event) {
    $snipwire = $this->wire('snipwire');
    if (!$snipwire) return;

    $page = $event->arguments(0);
    if ($snipwire->isProductTemplate($page->template)) {
        if ($page->hasfield('snipcart_item_id')) $page->setAndSave('snipcart_item_id', $page->id);
    }
}

/**
 * Check if the SKU value is unique across all product pages.
 * (Method triggered after Pages saveReady -> just before page is saved)
 *
 * @throws WireException
 *
 */
public function checkSKUUnique(HookEvent $event) {
    $snipwire = $this->wire('snipwire');
    if (!$snipwire) return;

    $page = $event->arguments(0);
    if ($snipwire->isProductTemplate($page->template)) {
        $field = $page->getField('snipcart_item_id');
        $sku = $page->snipcart_item_id; // SKU field value
        bd($sku);
        
        if ($page->isChanged('snipcart_item_id')) {
            $exists = $this->wire('pages')->get("snipcart_item_id=$sku");
            if ($exists->id) {
                // value is not unique!
                $error = $this->_('SKU must be unique'); 
                $exception = sprintf(
                    $this->_('SKU [%s] is already in use'),
                    $sku
                ); 
                $inputfield = $page->getInputfield($field);
                $inputfield->error($error); 
                throw new WireException($exception); // Prevent saving of non-unique value!
            }
        }
    }
}

 

Share this post


Link to post
Share on other sites

I knew there was something added lately, but then I didn't find it and thought maybe I was dreaming 😄

0A7ayjQ.png

  • Like 1

Share this post


Link to post
Share on other sites
15 hours ago, Zeka said:

This looks promising! Thank you. The problem is, I can't use this new class in my module as the PW version requirement is set to 3.0.148. I could borrow the code from this class and build my own method. This would require to rewrite a lot of code instead of getting the hooks working properly.

Share this post


Link to post
Share on other sites
15 hours ago, bernhard said:

I knew there was something added lately, but then I didn't find it and thought maybe I was dreaming 😄

0A7ayjQ.png

Thanks @bernhard, before I asked for help here, I searched for related information back and forth but couldn't find any relevant infos. The search results from your screenshot also didn't bring any light into this... 🙂

Share this post


Link to post
Share on other sites

Hi @Gadgetto the screencast was meant to show that I did search for the "unique" feature in the blog but didn't find relevant results 😉 It was not meant as something useful for your problem.

To your problem... Such things are a little hard when one is not directly in the code! Could you please setup two example hooks for the BASIC-PAGE template and the TITLE field. Almost everybody has those available for testing so we could just copy&paste your hooks into site/ready.php and provide tested solutions.

One thing that I saw in your code that could be slightly improved:

// your version:
if ($snipwire->isProductTemplate($page->template)) { ...

// better:
if($page->isSnipProduct) { ...
$wire->addHookProperty("Page::isSnipProduct", function($event) {
	$snipwire = ...
	$event->return = $snipwire->isProductTemplate($event->object->template);
}

That's just a little detail but IMHO overall that makes code more readable...

Another option would be to use the new Page Classes, but I guess that's no option for you because you because of the version requirement 🙂 

  • Like 1

Share this post


Link to post
Share on other sites

OK, here is the simplified code for testing in site/ready.php with "title" field used:

// To test, add this to your sites/ready.php

wire()->addHookAfter('Pages::added', 'presetTitleField', ['priority' => 99]);
wire()->addHookAfter('Pages::saveReady', 'checkTitleUnique');

function presetTitleField(HookEvent $event) {
    $page = $event->arguments(0);
    $page->setAndSave('title', 'Test' . $page->id);
}

function checkTitleUnique(HookEvent $event) {
    $page = $event->arguments(0);
    $field = $page->getField('title');
    $title = $page->title;
        
    if ($page->isChanged('title')) {
        $exists = wire('pages')->get("title=$title");
        if ($exists->id) {
            // value is not unique!
            $error = __('Title must be unique'); 
            $exception = sprintf(
                __('Title [%s] is already in use'),
                $title
            ); 
            $inputfield = $page->getInputfield($field);
            $inputfield->error($error); 
            throw new WireException($exception); // Prevent saving of non-unique value!
        }
    }
}

And thanks for the hint to extend the Page class with a new method!

Share this post


Link to post
Share on other sites

What I'm not sure about is: If I create a new page with the page editor, the creation process is split into 2 steps. In the first step you enter the title + name. Then you hit "Save". In the second step you can complete all other page fields and options.

Are the other page fields (beside title and name) already available for API in first step? Probably not. So my test code from last post could work with "title" field but not with the other page fields?

Share this post


Link to post
Share on other sites

This works for me:

// To test, add this to your sites/ready.php
wire()->addHookAfter('Pages::saveReady', 'checkiconUnique');

function checkiconUnique(HookEvent $event) {
  $page = $event->arguments(0);
  $field = $page->getField('icon');
  $icon = $page->icon;

  // preset icon
  // before the page is saved for the first time it does not have an id
  if(!$page->id) {
    $page->icon = "Preset icon " . uniqid();
  }
  else {
    if(!$page->isChanged('icon')) return;
    $exists = wire('pages')->get("id!=$page,icon=$icon");
    if($exists->id) {
      // value is not unique!
      $error = __('icon must be unique'); 
      $exception = sprintf(
        __('icon [%s] is already in use'),
        $icon
      ); 
      $inputfield = $page->getInputfield($field);
      $inputfield->error($error); 
      throw new WireException($exception); // Prevent saving of non-unique value!
    }
  }
}

Not sure if that works in all circumstances - that's why we build modules for such things. And not sure if a module by Ryan is really a 3rd party module that one should not include in his module 😉 But maybe it helps nonetheless...

I tested it with an "icon" field. You can simple find&replace that string to your fieldname. I made all occurrences lowercase to make that easier.

  • Like 1

Share this post


Link to post
Share on other sites
2 hours ago, Gadgetto said:

Are the other page fields (beside title and name) already available for API in first step?

    $this->addHookAfter('ProcessPageAdd::buildForm', function ($event) {
        $form = $event->return;
        // If adding page under particular parent
        if ($this->input->parent_id === '1041') {
            // Add inputfield to form after existing title field
            // If you name the inputfield the same as the field you want to populate
            // then the entered value will be saved automatically
            $f              = $this->modules->InputfieldText;
            $f->name        = 'myfieldname';
            $f->label       = 'My Field Label';
            $f->description = 'my field description';
            $f->required    = true;
            $f->attr('required', 1); // use HTML required attribute too
            $title_field        = $form->getChildByName('title');
            $title_field->label = 'Page Title';
            $title_field->description = "page title description";
            $form->insertAfter($f, $title_field);
            $event->return = $form;
        }
    });

I got this code a long time ago from somebody in this forum, and it works nicely. I can't find the original thread right now. It basically renders the field "myfieldname" in the first screen when you create a new page under parent id 1041. Not sure if that helps for your scenario.

  • Like 1

Share this post


Link to post
Share on other sites

@bernhard, @dragan,

Thanks a lot guys, I could fix it! It now runs bombproof  - this is the final code:

    public function checkSKUUnique(HookEvent $event) {
        $snipwire = $this->wire('snipwire');
        if (!$snipwire) return;
        
        $page = $event->arguments(0);
        if ($snipwire->isProductTemplate($page->template)) {
            $field = $page->getField('snipcart_item_id');
            $sku = $page->snipcart_item_id; // SKU field value
            if (!$sku) return; // <- ###### only had to add this line
            
            if ($page->isChanged('snipcart_item_id')) {
                $exists = $this->wire('pages')->get("snipcart_item_id=$sku, id!=$page, status<" . Page::statusTrash); // <- ###### and change this line
                if ($exists->id) {
                    // value is not unique!
                    $error = $this->_('SKU must be unique'); 
                    $exception = sprintf(
                        $this->_('SKU [%s] is already in use'),
                        $sku
                    ); 
                    $inputfield = $page->getInputfield($field);
                    $inputfield->error($error); 
                    throw new WireException($exception); // Prevent saving of non-unique value!
                }
            }
        }
    }

 

Share this post


Link to post
Share on other sites

Glad you solved it. Beside what I've already mentioned regarding isProductTemplate I have another suggestion: I think you could avoid neting IFs and make the code even more readable:

public function checkSKUUnique(HookEvent $event) {
  $snipwire = $this->wire('snipwire');
  if (!$snipwire) return;
  
  // only check snip products
  $page = $event->arguments(0);
  if(!$page->isSnipProduct) return;

  // exit if no changes
  if(!$page->isChanged('snipcart_item_id')) return;

  // don't check unsaved pages
  $field = $page->getField('snipcart_item_id');
  $sku = $page->snipcart_item_id; // SKU field value
  if(!$sku) return;

  // exit if value is unique
  $exists = $this->wire('pages')->get([
    ['snipcart_item_id', '=', $sku],
    ['id', '!=', $page],
    ['status', '<', Page::statusTrash],
  ]);
  if(!$exists->id) return;

  // value is not unique, show errors
  $error = $this->_('SKU must be unique');
  $page->getInputfield($field)->error($error);
  $exception = sprintf($this->_('SKU [%s] is already in use'), $sku);
  throw new WireException($exception); // Prevent saving of non-unique value!
}

 

  • Like 2

Share this post


Link to post
Share on other sites
5 hours ago, bernhard said:

I think you could avoid nesting IFs and make the code even more readable

You are right @bernhard. And I didn't even know I could set selectors via array! I'm still scratching on the surface of ProcessWire...

Code refactoring will be done when the module is in a stable state and I've implemented all necessary features. 

  • Like 1

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By Andi
      Continuing my journey into PW hooks, I'm trying to find a way to retrieve all images from a page that explicitly *do not* have a certain tag (or tags) attached to them.
      Found this post from 2015
      But I'm wondering if there's a more elegant way to go about this.
      Let's say I have a multi-image field called "images_header" and instead of
      $page->images_header->findTag('mytag'); I would like to do this:
      $page->images_header->excludeTag('mytag'); So I'd be able to do
      // find images that don't have the tag "mytag" $images = $page->images_header->excludeTag('mytag'); // check if there's any images if (count($images)>0) { // do something.. } Would this be possible by hooking into Pagefiles somehow?
      There's this bit in /wire/core/Pagefiles.php Line 626 that I'd basically just need to reverse (or at least in my mind 😄 )
      public function findTag($tag) { $items = $this->makeNew(); foreach($this as $pagefile) { if($pagefile->hasTag($tag)) $items->add($pagefile); } return $items; } Any ideas on how this could be done in a graceful manner?
      Thanks in advance!
    • By Andi
      Getting a little deeper into the ProcessWire state-of-mind here. I seriously think I wouldn't have come back to webdev if it wasn't for this wonderful little gem of a CMS.
      I have an "Options" field added to all users on a site. If the user has anything other then "default" selected, I would like to show a permanent message in the admin like the one in the screenshot, only so that the user can't close it. As a friendly reminder that he changed that option from default to something crazy 🙂
      I've read up on how to send messages to users, but where would I hook into to make this show up all the time in the backend?
      https://processwire.com/api/ref/wire/message/
      Thanks in advance!

    • By celfred
      Hello,
       
      Here's what I'm trying to achieve : I have a textarea field that is frontend editable on any page I want to. If a user with a specific role updates it, I want to tick a checkbox field on the parent page where the textarea resides. So I tried to hook as follows in my ready.php file :

        $this->addHookBefore('Fieldtype::savePageField', function(HookEvent $event) {       $page = $event->argument[0];       $field = $event->argument[1];       if($this->user->hasRole('teacher')) {      $page->alertBox = 1;       } // DEBUG HERE (bd($page),  l($field)... ????   }); Of course (!) it doesn't work (yet !) but the thing is I have no idea how to debug this since my bd() never triggers. I've tried the 'Event interceptor' of Tracy debugger which helped me setting up my hook. I guess my $page and $field are correct... but how could I go any further ?
      The road is long to become a dev (when you're not one 🙂 )...
      For further information in case someone wonders : I'd like to set up a textarea that is modified by a user (having a 'player' role, ie 'student'). When front-end modified, I wish it would automatically alert me (the teacher) by ticking a box. So when I log in with my teacher role, I get a list of all textareas I have to read over. When I read/correct/update them (front-end), I would like my hook to automatically untick the box to remove it from my list. In other words, the 'textarea' status should go back and forth according to who modified it last. For the time being, I have managed to make it work with a checkbox that the user has to manually tick/untick, but I've noticed many kids forget to tick the box so they edit their text and I don't get a notice 😞
      Thanks if anyone takes time to try and help me on that one !
    • By celfred
      Hello,
      I'm struggling with hooks to try and do this : adding user page to a page field when visiting a particular page on my website. In fact, I want to have a list of connected users on a particular page (called meeting-hall).
      I've tried all kinds of things (which I don't really understand, sorry) but the latest is this :

        $wire->addHookBefore('Page::render', function($e) {     $page = $e->arguments(0);     if ($page->is("name=meeting-hall")) {       $page->connected->add($user); // connected is a Page field       $page->save();     } else {       $meeting = wire("pages")->get("name=meeting-hall");       $meeting->connected->remove($user);       $meeting->save();     }   }); I put this in my _init.php file but nothing works...
      The best I managed was updating my 'connected' field when the user loads the 'meeting hall' page by having this on the page template :

          if ($user->isLoggedin()) {       $page->connected->add($user);       $page->of(false);       $page->save();       $page->of(true);     } But I wanted to remove the user when he or she leaves the page... hence my thought about using hooks...
      If you can give me a hint, I'd greatly appreciate !
    • By t0b1
      Hello there, and thanks for ProcessWire!
      I'm getting to know ProcessWire while doing my first project using it and I really like it so far.
      The challenge I'm facing right now is the following:
      I have a One-Pager using fullpage.js, realized as a single PW-Page containing a Repeater Field where each Repeater Item is one Section.
      Some Sections are supposed to have a little menu at the Top which references/links to different Sections of the Website, so I wanted to use a Checkbox Field "Top Menu" to decide if a Section gets a Menu and a Page Reference Field to choose the different Sections (Repeater Items) it should contain.
      I've already accomplished this by pasting the following code into /site/ready.php:
       
      $wire->addHookAfter('InputfieldPage::getSelectablePages', function($event) { if($event->object->hasField == 'top_menu_entries') { $page = $event->arguments('page'); if($page instanceof RepeaterPage) $page = $page->getForPage(); $event->return = $page->; } });  
      The only problem that still remains is that when I select the Entry of the Repeater Item itself it doesnt save the selection, meaning after I saved it's unselected again.
      On some Sections I do want a Menu-Entry for the Section itself though (which would be styled differently and not link anywhere) for Continuity-Reasons, any ideas on how to achieve that?
×
×
  • Create New...