Skip to content

PHP

views

Foreach with ampersand (&)

When iterating through an array of items, changes you make to the as value are not reflected in the original array. e.g. if you iterate through your array as in foreach ($cars as $car) any changes to $car will not be reflected in $cars.

The following code shows the problem if you don't use the ampersand(&).

php

$correlations = [
  [
    'completion_status' => 'complete',
  ],
  [
    'completion_status' => 'incomplete',
  ],
  [
    'completion_status' => 'incomplete',
  ],
];

foreach ($correlations as $correlation) {
  $correlation['action_status'] = ($correlation['completion_status'] == 'complete') ? 'no-action-required' : 'action-required';
}
var_dump($correlations);

// Notice the use of the ampersand below.
foreach ($correlations as &$correlation) {
  $correlation['action_status'] = $correlation['completion_status'] == 'complete' ? 'no-action-required' : 'action-required';
}
var_dump($correlations);

And here is the output. Notice that the first var_dump shows no sign of the action_status items. With the ampersand(&) the expected variable appears.

array(3) {
  [0]=>
  array(1) {
    ["completion_status"]=>
    string(8) "complete"
  }
  [1]=>
  array(1) {
    ["completion_status"]=>
    string(10) "incomplete"
  }
  [2]=>
  array(1) {
    ["completion_status"]=>
    string(10) "incomplete"
  }
}
array(3) {
  [0]=>
  array(2) {
    ["completion_status"]=>
    string(8) "complete"
    ["action_status"]=>
    string(18) "no-action-required"
  }
  [1]=>
  array(2) {
    ["completion_status"]=>
    string(10) "incomplete"
    ["action_status"]=>
    string(15) "action-required"
  }
  [2]=>
  &array(2) {
    ["completion_status"]=>
    string(10) "incomplete"
    ["action_status"]=>
    string(15) "action-required"
  }
}

Here is Copilot's take on the problem:

The code you provided is iterating over the $correlations array and updating each $correlation item's action_status based on its completion_status. However, this code doesn't actually update the original $correlations array because $correlation is a copy of the original item in the array, not a reference.

To refactor this code and make sure the original $correlations array is updated, you should use the & symbol to pass $correlation by reference:

php
foreach ($correlations as &$correlation) {
    $correlation['action_status'] = $correlation['completion_status'] == 'complete' ? 'no-action-required' : 'action-required';
}
unset($correlation); // Unset reference to avoid unexpected behavior

This way, any changes made to $correlation inside the loop will be reflected in the original $correlations array. The unset($correlation) line is added after the loop to break the reference with the last element, as recommended in the PHP documentation to avoid unexpected behavior.

Deep merge arrays with numeric keys

While searching for a way to merge two arrays with numeric keys, I found this information on PHP.net. Unfortunately array_merge_recursive() does not work with numeric keys. The following code (from 12 years ago) is a solution to the problem. I did use ChatGPT to modernize the code for PHP 8.

php
function array_merge_recursive_new(array $base, array $array1, array ...$arrays): array {
  array_unshift($arrays, $array1);

  foreach ($arrays as $array) {
    foreach ($array as $key => $value) {
      if (is_array($value) && isset($base[$key]) && is_array($base[$key])) {
        $base[$key] = array_merge_recursive_new($base[$key], $value);
      } else {
        $base[$key] = $value;
      }
    }
  }
  return $base;
}

// Define some test arrays
$array1 = ['a' => 1, 'b' => 2, 'c' => ['d' => 3]];
$array2 = ['a' => 2, 'c' => ['e' => 4]];
$array3 = ['f' => 5, 'c' => ['g' => 6]];

// Merge the arrays
$result = array_merge_recursive_new($array1, $array2, $array3);

// Print the result
print_r($result);

Here is the output:

Array
(
    [a] => 2
    [b] => 2
    [c] => Array
        (
            [d] => 3
            [e] => 4
            [g] => 6
        )
    [f] => 5
)

Match

PHP 8.0 introduced the match expression, which is similar to a switch statement but with a more concise syntax. Here's an example of how you can use match:

php
  /**
   * {@inheritdoc}
   */
  public function isEmpty(): bool {
   return match ($this->get('value')->getValue()) {
     NULL, '' => TRUE,
     default => FALSE,
   };
  }

It allows you to match a value against multiple conditions and return a result based on the matched condition. In this example, the isEmpty method checks if the value of the field is NULL or an empty string and returns TRUE if it matches, otherwise it returns FALSE.

More at php.net.

Field mapping

Here is the most convoluted way (Don't ask.) I could figure out to insert some additional instructions on a form upload field in a media entity. It showed some interesting ways to get deep into the render array. This line: $field_reference = &$form; establishes a starting point for navigating the form's structure. By using a reference (&), any changes made to $field_reference will directly affect the $form array, ensuring that the alterations are applied to the actual form being processed.

The loop below navigates through the form array to the specific field that needs modification. It does so by updating $field_reference at each step to point deeper into the form structure, following the path defined in the mapping. This dynamic navigation allows the code to reach any field within the form, regardless of its depth or location.

php
foreach ($mapping['field_path'] as $path) {
  $field_reference = &$field_reference[$path];
}

Here is the whole function:

php
/**
 * Implements hook_form_alter().
 */
function abc_form_alter(&$form, FormStateInterface $form_state, $form_id) {
    // Mapping of form IDs to their respective fields and new descriptions.
    $message = t('Keep titles clear and useful. e.g., "ski_trip_2023" instead of "img_pxl_443445"<br>');
  $form_mappings = [
    'media_library_add_form_upload' => [
      'field_path' => ['container', 'upload'],
      'description' => $message,
    ],
    'media_image_add_form' => [
      'field_path' => ['field_media_image', 'widget', 0],
      'description' => $message,
    ],
    'media_image_edit_form' => [
      'field_path' => ["replace_file", "replacement_file"],
      $message,
    ],
  ];
  // Check if the current form ID is in the mappings.
  if (array_key_exists($form_id, $form_mappings)) {
    // Retrieve the mapping for the current form.
    $mapping = $form_mappings[$form_id];
    // Build the reference to the form field.
    $field_reference = &$form;
    foreach ($mapping['field_path'] as $path) {
      $field_reference = &$field_reference[$path];
    }
    // Insert the new description before the existing one.
    $field_reference['#description'] = $mapping['description'] . '<br>' . $field_reference['#description'];
  }
}

For those of you following along at home, you may realize that this could easily be performed with a simple hook_form_FORM_ID_alter() implementation for each form. Some developers think this is a better way to do things. You decide.

The difference between require and include

The difference between include and require is subtle.

Require, as the function name suggests requires that the included file exist to continue the script. So, if require fails, the script stops.

Using include will allow the script to continue. Most of the time, using require makes more sense because it's likely that the file we want to include includes some important information that is required for your application to run properly.

e.g.

php
if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {
  include $app_root . '/' . $site_path . '/settings.local.php';
}

// Automatically generated include for settings managed by ddev.
$ddev_settings = dirname(__FILE__) . '/settings.ddev.php';
if (getenv('IS_DDEV_PROJECT') == 'true' && is_readable($ddev_settings)) {
  require $ddev_settings;
}

nullsafe operator

The nullsafe operator ?-> is a new feature in PHP 8 that allows you to safely access properties and methods of an object without having to check if the object is null. This means you only need 1 null check as the operator will return null if any of the calls in the chain return null.

php
 $job = $this->apiHandler?->getJobs()?->getJobById($job_id);
 // Any of the calls can return NULL
 if ($job === NULL) {
  continue;
 }

Note

The ?-> signs after each call. If any of the calls in the chain return null, the entire chain will return null.

PHP Nullsafe Operator

Regex examples

php
// Remove Lab and a space from the beginning of a string.
$bundle_label = preg_replace('#^(Lab )#i', '', $bundle_label);
php
// Remove Team Content regardless of case from the beginning of a string.
// #i is the case-insensitive flag.
$bundle_label = preg_replace('#^(Team Content)#i', 'Team', $bundle_label);
php
// Create human friendly ids for the top level paragraphs for the tabs
// Match strings that start with "lab_", "office_", or "staff_profile_"
$start_pattern = '/^(lab|office|staff_profile)_/';
if (preg_match($start_pattern, $entity->bundle())) {
  // Remove the matched prefix.
  $id = preg_replace($start_pattern, '', $id);
}
php
// Remove the suffix _tab from the bundle name.
// e.g. if the bundle is "lab_tab" it will be changed to "lab"
$end_pattern = '/_(tab)$/';
if (preg_match($end_pattern, $entity->bundle())) {
  $id = preg_replace($end_pattern, '', $id);
}

Return early pattern

I am a fan of the return early pattern. Return early is the way of writing functions or methods so that the expected positive result is returned at the end of the function and the rest of the code terminates early by returning or throwing an exception if there are any errors. I've also seen this called the "happy path" pattern.

Doesn't this code make you feel good? It is from the medium article linked above.

php
public String returnStuff(SomeObject argument1, SomeObject argument2){
      if (!argument1.isValid()) {
            throw new Exception();
      }

      if (!argument2.isValid()) {
            throw new Exception();
      }

      SomeObject otherVal1 = doSomeStuff(argument1, argument2);

      if (!otherVal1.isValid()) {
            throw new Exception();
      }

      SomeObject otherVal2 = doAnotherStuff(otherVal1);

      if (!otherVal2.isValid()) {
            throw new Exception();
      }

      return "Stuff";
}

While you are contemplating this, consider these anti-patterns: Else is considered smelly and Arrow Anti-Pattern where code becomes shaped like an arrow because of nested conditions and loops like:

 if
   if
     if
       if
         do something
       endif
     endif
   endif
 endif

Replace special characters

This code uses array_map to create a new array where each special character is prefixed with a backslash (\). This effectively escapes each character. For example, + becomes \+, * becomes \*, etc.

php
$special_chars = ['\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', '/'];
$v = str_replace($special_chars, array_map(fn($char) => '\\' . $char, $special_chars), $v);

If $v is "Hello+World!", after running this code, $v would become "Hello\+World\!".

Reference