Structure field to store tags and their translations using UUIDs

Hi dear community :smiley:

I’m currently working on an multi-language portfolio where projects have categories acting as filters. I needed a way to manage all those categories in one place in the panel, so when some are renamed for example, I don’t have to manually update each project page where they could have already been attributed.

In order to do this, I created a structure field in a Projects blueprint that stores all the category names in English and French, with custom UUIDs generated by a hook on page update.

Here is the blueprint:

categories:
  label: Categories
  type: structure
  translate: false
  fields:
    category:
      label: English
      type: text
    categorie:
      label: Français
      type: text
    uuid:
      type: hidden

Here is the hook in the config.php:

'page.update:after' => function ($newPage, $oldPage) {
  if ($newPage->intendedTemplate()->name() == 'projects') {
    $categories = $newPage->categories()->yaml();
    foreach($categories as &$cat) {
      if (!isset($cat["uuid"]) || !$cat["uuid"]) {
        $cat["uuid"] = 'cat' . substr(str_replace('-', '', Str::uuid()), 0, 8);
      }
    }
    $newPage->update(['categories' => Data::encode($categories, 'yaml')]);
  }
}

And here is how everything is stored:

- 
  category: Exhibition
  categorie: Exposition
  uuid: catdcdb7f16
- 
  category: Graphic design
  categorie: Graphisme
  uuid: cat87d09ebe

Then in a Project blueprint, I added this multiselect field querying the structure:

categories:
  label: Categories
  type: multiselect
  translate: false
  options:
    type: query
    query: page.parent.categories.toStructure
    text: "{{ item.category }}"
    value: "{{ item.uuid }}"

Which stores selected UUIDs like:

Categories: catdcdb7f16, cat87d09ebe

I reeeeally don’t know if it’s the best solution to do so, but right now, it seems to work in the panel… However, I miss the solution to simply retrieve the category names based on the UUIDs on the front, depending on the language displayed, so let’s say:

$page->categoryname();
// outputs ‘Exhibition / Graphic design’ or ‘Exposition / Graphisme’

$page->categoryclass();
/* outputs ‘exhibition graphic-design’ or ‘exposition graphisme’ */

I’ve created a Project model with public functions… but well… I’m stuck here. I don’t know how to code this properly, if map functions should be used (if yes, how?), if I should go with a foreach loop on the structure and findBy('uuid', $this->categories()). Here is what I have at the moment, after maaaany attempts (please don’t judge me :poop:):

public function categoryname() {
  $language = kirby()->language();
  $allcategories = $this->parent()->categories()->toStructure();
  $projectcategories = $this->categories()->split(',');

  if($language == 'fr'){
    $projectcategories = 
  }
  else {
    $projectcategories =
  }
  
  $categoryname = implode(' / ', $projectcategories);
  return $categoryname;
};

Could someone help me please? :pray:

Something like this should work:

public function categoryname() {
  $language = kirby()->language();
  $allcategories = $this->parent()->categories()->toStructure();
  $projectcategories = $this->categories()->split(',');
  $filtered = $allcategories->filter(fn ($item) => in_array($item->uuid(), $projectcategories);

  $field = $language->code() === 'fr' ? 'categorie' : 'category';

  return implode(' / ', $filtered->pluck($field, ',', true);
}
1 Like

Hi Sonja, and thanks for your prompt reply. It woooooorks!!

However, the categories are outputted in the same order as in the parent structure field… whereas it should be like selected and displayed in the multiselect field :thinking:

Wouldn’t be better in this case to use a map function? (I couldn’t find any cookbook explaining how it works BTW…)

You can find map examples here:

and

1 Like

I finally managed a way to achieve what I expected. The code may not be very clean (especially the language part), but it works :partying_face:

Thanks again for your input Sonja!

public function categorynames() {
  $language = kirby()->language()->code();
  $allcategories = $this->parent()->categories()->toStructure();
  $projectcategories = $this->categories()->split(',');
  
  foreach ($projectcategories as $cat){
    $item = $allcategories->findBy('uuid', $cat);
    if($language === 'fr'){
      $categorynames[] = $item->categorie();
    } else {
      $categorynames[] = $item->category();
    }
  }

  return implode(' / ', $categorynames);
}