#How to get PIM category tree

Use case:
App Workflow
PIM Features:
Categories
REST API endpoint(s):
categories
If you're starting building your App, make sure you previously followed:

#Context

A category tree is typically a graph where nodes are categories with or without sub-categories. The first node - the root of the graph - is the root category of the category tree.

relationship schema

Get the big picture here.

Fortunately, getting the whole category is as simple as querying the correct API endpoint.

#Fetch the category tree

#0. Initialization


    function buildApiClient(): GuzzleHttp\Client
    {
        $pimUrl = 'https://url-of-your-pim.com';
        $appToken = 'your_app_token'; // Token provided during oAuth steps
    
        // If you haven't done it yet,
        // please follow the Guzzle official documentation to install the client 
        // https://docs.guzzlephp.org/en/stable/overview.html#installation
    
        // Set your client for querying Akeneo API as follows
        $client = new \GuzzleHttp\Client([
            'base_uri' => $pimUrl,
            'headers' => ['Authorization' => 'Bearer ' . $appToken],
        ]);
    }
    

    // Install the node-fetch library by following the official documentation:
    // https://www.npmjs.com/package/node-fetch
    import fetch from 'node-fetch';
    
    const pimUrl = 'https://url-of-your-pim.com';
    const accessToken = 'your_app_token'; // Token provided during oAuth steps
    
    // Set your client for querying Akeneo API as follows
    async function get(url, accessToken) {
      return await fetch(url, {
        headers: {
          'Authorization': `Bearer ${accessToken}`
        }
      });
    }
    

#1. Get the PIM structure by fetching a channel from API

Workflow:

synchronisation steps

Collect flat category tree from PIM API:


    function getCategories(): void
    {
        $client = buildApiClient();
    
        // Get root category code from storage
        $rootCategoryCode = getRootCategoryCode();
    
        $maxItems = 100;
    
        $nextUrl = sprintf(
            '/api/rest/v1/categories'
            . '?with_position=true'
            . '&search={"parent":[{"operator":"=","value":"%s"}]}'
            . '&limit=%s',
            $rootCategoryCode,
            $maxItems
        );
    
        $categoryPages = [];
    
        do {
            $response = $client->get($nextUrl);
            $data = json_decode($response->getBody()->getContents(), true);
            $categoryPages[] = $data['_embedded']['items'];
    
            $nextUrl = $data['_links']['next']['href'] ?? null;
        } while (
            $nextUrl
        );
    
        // Adds root category to complete the list.
        $categoryPages[][] = getRootCategory($rootCategoryCode);
    
        $categories = array_merge(...$categoryPages);
    
        // Save categories into storage
        storeCategories($categories);
    }
    
    function getRootCategory(string $code): array
    {
        $client = buildApiClient();
    
        $response = $client->get('/api/rest/v1/categories/' . $code);
        $rootCategory = json_decode($response->getBody()->getContents(), true);
    
        if ($rootCategory['parent'] !== null) {
            throw new \InvalidArgumentException(
                sprintf('Category %s is not root.', $code)
            );
        }
    
        return $rootCategory;
    }
    

    async function fetchCategories() {
      // Get root category code from storage
      const rootCategoryCode = await getRootCategoryCode();
      const maxItems = 100;
    
      let nextUrl = `${pimUrl}/api/rest/v1/categories`
        + '?with_position=true'
        + `&search={"parent":[{"operator":"=","value":"${rootCategoryCode}"}]}`
        + `&limit=${maxItems}`
      ;
    
      const categories = [];
    
      do {
        const response = await get(nextUrl);
        const data = await response.json();
        const newCategories = data._embedded.items;
        categories.push(...newCategories);
    
        nextUrl = data._links?.next?.href;
      } while (nextUrl)
    
      // Adds root category to complete the list.
      const rootCategory = await getRootCategory(rootCategoryCode);
      categories.push(rootCategory);
    
      // Save categories into storage
      storeCategories(categories);
    }
    
    async function getRootCategory(rootCategoryCode) {
      const pimUrl = process.env.PIM_URL;
    
      const response = await get(`${pimUrl}/api/rest/v1/categories/${rootCategoryCode}`);
      return await response.json();
    }
    

The categories are retrieved in a flat array like this:


    var_export($categories);
    
    // Output
    [
        [
            'code' => 'master',
            'parent' => null,
            'updated' => '2022-01-01T00:00:00+00:00',
            'labels' => [
                'en_US' => 'Master catalog',
                'fr_FR' => 'Catalogue principal',
                'de_DE' => 'Hauptkatalog',
            ]
        ],
        [
            'code' => 'tvs_projectors',
            'parent' => 'master',
            'updated' => '2022-11-22T14:40:43+00:00',
            'labels' => [
                'en_US' => 'TVs and projectors',
                'fr_FR' => 'Téléviseurs et projecteurs',
                'de_DE' => 'TVs und projectoren',
            ]
            'position' => 1, // Brought by 'with_position=true' option
        ],
        [
            'code' => 'cameras',
            'parent' => 'master',
            'updated' => '2022-11-22T14:40:43+00:00',
            'labels' => [
                'en_US' => 'Cameras',
                'fr_FR' => 'Cameras',
                'de_DE' => 'Cameras',
            ]
            'position' => 2,
        ],
        [
            'code' => 'digital_cameras',
            'parent' => 'cameras',
            'updated' => '2022-11-22T14:40:43+00:00',
            'labels' => [
                'en_US' => 'Digital cameras',
                'fr_FR' => 'Caméras digitales',
                'de_DE' => 'Digitale Kameras',
            ]
            'position' => 1,
        ],
        /* ... */
    ]
    

    console.log(categories);
    
    // Output
    [
      {
        "code": "master",
        "parent": null,
        "updated": "2022-01-01T00:00:00+00:00",
        "labels": {
          "en_US": "Master catalog",
          "fr_FR": "Catalogue principal",
          "de_DE": "Hauptkatalog",
        },
      },
      {
        "code": "tvs_projectors",
        "parent": "master",
        "updated": "2022-11-22T14:40:43+00:00",
        "labels": {
          "en_US": "TVs and projectors",
          "fr_FR": "Téléviseurs et projecteurs",
          "de_DE": "TVs und projectoren",
        },
        "position": 1, // Brought by 'with_position=true' option
      },
      {
        "code": "cameras",
        "parent": "master",
        "updated": "2022-11-22T14:40:43+00:00",
        "labels": {
          "en_US": "Cameras",
          "fr_FR": "Caméras",
          "de_DE": "Cameras"
        },
        "position": 2
      },
      {
        "code": "digital_cameras",
        "parent": "cameras",
        "updated": "2022-11-22T14:40:43+00:00",
        "labels": {
          "en_US": "Digital cameras",
          "fr_FR": "Caméras digitales",
          "de_DE": "Digitale Kameras"
        },
        "position": 2
      },
    ]
    

Using parent and position attributes, the corresponding category tree can be determined:

  • master
    • tv_projectors
    • cameras
      • digital_cameras

This is the end of the App workflow! Well done! Now that you have successfully synchronized the essential PIM data you can move on to your next app building task. If you need to sync the Assets and Reference Entities record stay tuned, more guided tutorials should follow up to assist you with this.