skip to Main Content

In Laravel, I’m performing a JSON map where I need to transform a JSON to a different schema. To do that I’m following these steps:

  1. Dot the array with Arr::dot helper. This way I have this result, example:
array(11) {
  ["0.identificador"]=>
  int(1)
  ["0.nome_completo"]=>
  string(8) "John Doe"
  ["0.email"]=>
  string(11) "[email protected]"
  ["0.empresa.razao_social"]=>
  string(11) "ABC Company"
  ["0.empresa.endereco"]=>
  string(11) "123 Main St"
  ["0.pedidos.0.SKU"]=>
  int(1)
  ["0.pedidos.0.descricao"]=>
  string(5) "Shoes"
  ["0.pedidos.0.qtd"]=>
  int(2)
  ["0.pedidos.1.SKU"]=>
  int(2)
  ["0.pedidos.1.descricao"]=>
  string(5) "Shirt"
  ["0.pedidos.1.qtd"]=>
  int(1)
}
  1. Iterate through the dotted array and compare with my keys map. If they match, I set the value with the mapped key.

My mapping looks like this:

array(8) {
    ["*.identificador"]=> string(4) "*.id"
    ["*.nome_completo"]=> string(6) "*.name"
    ["*.empresa"]=> string(9) "*.company"
    ["*.empresa.razao_social"]=> string(14) "*.company.name"
    ["*.empresa.endereco"]=> string(17) "*.company.address"
    ["*.pedidos.*.SKU"]=> string(13) "*.orders.*.id"
    ["*.pedidos.*.descricao"]=> string(18) "*.orders.*.product"
    ["*.pedidos.*.qtd"]=> string(19) "*.orders.*.quantity"
  }

And this is what my code looks like so far:


$payload = [
    [
        "identificador" => 1,
        "nome_completo" => "John Doe",
        "email" => "[email protected]",
        "empresa" => [
            "razao_social" => "ABC Company",
            "endereco" => "123 Main St"
        ],
        "pedidos" => [
            [
                "SKU" => 1,
                "descricao" => "Shoes",
                "qtd" => 2
            ],
            [
                "SKU" => 2,
                "descricao" => "Shirt",
                "qtd" => 1
            ]
        ]
    ]
];

$dottedPayload = Arr::dot($payload);

$transformedPayload = [];
foreach ($dottedPayload as $key => $value) {
    $newKey = preg_replace('/d+./', '*.', $key);
    $newKey = preg_replace('/.d+./', '.*.', $newKey);
    $newKey = preg_replace('/.d+$/', '.*', $newKey);

    $mappedKey = $mappingConfig[$newKey] ?? $key;

    data_fill($transformedPayload, $mappedKey, $value);
}

The output I have is this:

array(2) {
  ["*"]=>
  array(4) {
    ["id"]=>
    int(1)
    ["name"]=>
    string(8) "John Doe"
    ["company"]=>
    array(2) {
      ["name"]=>
      string(11) "ABC Company"
      ["address"]=>
      string(11) "123 Main St"
    }
    ["orders"]=>
    array(1) {
      ["*"]=>
      array(3) {
        ["id"]=>
        int(2)
        ["product"]=>
        string(5) "Shirt"
        ["quantity"]=>
        int(1)
      }
    }
  }
  [0]=>
  array(1) {
    ["email"]=>
    string(11) "[email protected]"
  }
}

But this is the expected:

$expectedResult = [
    [
        "id" => 1,
        "name" => "John Doe",
        "email" => "[email protected]",
        "company" => [
            "name" => "ABC Company",
            "address" => "123 Main St"
        ],
        "orders" => [
            [
                "id" => 1,
                "product" => "Shoes",
                "quantity" => 2
            ],
            [
                "id" => 2,
                "product" => "Shirt",
                "quantity" => 1
            ]
        ]
    ]
];

How can I properly undot the array after the mapping, with the wildcards, setting the indexes correctly? Any other suggestions to perform this JSON mapping are also welcomed.

2

Answers


  1. I like to implement data mapping using the Spatie Laravel Data package.

    The Spatie package uses classes to define data objects which are filled from arrays or json data. The names of the properties are the field names you want to use in your code and the package provides a MapInputName attribute modifier to automatically translate incoming data names.

    First we define the data classes and the field mapping:

    use SpatieLaravelDataAttributesDataCollectionOf;
    use SpatieLaravelDataData;
    use SpatieLaravelDataAttributesMapInputName;
    use SpatieLaravelDataDataCollection;
    
    class Company extends Data
    {
      public function __construct(
        #[MapInputName('razao_social')] 
        public string $name,
        #[MapInputName('endereco')] 
        public string $address
      ) { }
    }
    
    class Order extends Data
    {
      public function __construct(
        #[MapInputName('SKU')] 
        public int $id,
        #[MapInputName('descricao')] 
        public string $product,
        #[MapInputName('qtd')] 
        public int $quantity
      ) { }
    }
    
    class Payload extends Data
    {
      public function __construct(
        #[MapInputName('identificador')] 
        public int $id,
        #[MapInputName('nome_completo')] 
        public string $name,
        #[MapInputName('empresa')]
        public Company $company,
        #[MapInputName('pedidos')]
        #[DataCollectionOf(Order::class)]
        public DataCollection $orders
      ) { }
    }
    

    When you receive the payload data pass it to the static constructor from(...) or use dependency injection to get data from a Request:

    $payload = [
        [
            "identificador" => 1,
            "nome_completo" => "John Doe",
            "email" => "[email protected]",
            "empresa" => [
                "razao_social" => "ABC Company",
                "endereco" => "123 Main St"
            ],
            "pedidos" => [
                [
                    "SKU" => 1,
                    "descricao" => "Shoes",
                    "qtd" => 2
                ],
                [
                    "SKU" => 2,
                    "descricao" => "Shirt",
                    "qtd" => 1
                ]
            ]
        ]
    ];
    
    Payload::from($payload[0])->toArray();
    

    The toArray function gives the output:

    [
      "id" => 1,
      "name" => "John Doe",
      "company" => [
        "name" => "ABC Company",
        "address" => "123 Main St",
      ],
      "orders" => [
        [
          "id" => 1,
          "product" => "Shoes",
          "quantity" => 2,
        ],
        [
          "id" => 2,
          "product" => "Shirt",
          "quantity" => 1,
        ],
      ],
    ]
    

    Not only does the package provide these mappings but it can also validate the data through attribute modifiers and be passed to Model::create to make Eloquent objects easily.

    Login or Signup to reply.
  2. As you want the mapping of key names to be user configurable you can modify your original approach to use capture groups, these keep the numeric counters inserted by the Arr::dot function.

    $payload = [
        [
            "identificador" => 1,
            "nome_completo" => "John Doe",
            "email" => "[email protected]",
            "empresa" => [
                "razao_social" => "ABC Company",
                "endereco" => "123 Main St"
            ],
            "pedidos" => [
                [
                    "SKU" => 1,
                    "descricao" => "Shoes",
                    "qtd" => 2
                ],
                [
                    "SKU" => 2,
                    "descricao" => "Shirt",
                    "qtd" => 1
                ]
            ]
        ]
    ];
    
    $regexMapping = [
      'identificador' => 'id',
      'nome_completo' => 'name',
      'email' => 'email',
      'empresa.razao_social' => 'company.name',
      'empresa.endereco' => 'company.address',
      'pedidos.(d+).SKU' => 'orders.$1.id',
      'pedidos.(d+).descricao' => 'orders.$1.product',
      'pedidos.(d+).qtd' => 'orders.$1.quantity',
    ];
    
    $dotted = Arr::dot($payload);
    $newPayload = [];
    $replacements = 0;
    
    foreach ($dotted as $key => $value) {
      foreach ($regexMapping as $pattern => $replacement) {
        $newKey = preg_replace('/'.$pattern.'/', $replacement, $key, -1, $replacements);
        if ($replacements) {
          // only store the new key if we replaced something
          $newPayload[$newKey] = $value;
          // match found, no need to check any more patterns
          break; 
        }
      }
    }
    
    Arr::undot($newPayload);
    

    Which gives the result:

    [
      [
        "id" => 1,
        "name" => "John Doe",
        "email" => "[email protected]",
        "company" => [
          "name" => "ABC Company",
          "address" => "123 Main St",
        ],
        "orders" => [
          [
            "id" => 1,
            "product" => "Shoes",
            "quantity" => 2,
          ],
          [
            "id" => 2,
            "product" => "Shirt",
            "quantity" => 1,
          ],
        ],
      ],
    ]
    

    The inefficiency in this approach is the nested loop; but in order to understand how it works, and if you only have a few mappings, it’s not a big concern.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search