Пример обработки PUT-запроса на PHP

Пример обработки PUT-запроса на PHP
Содержание:
  1. Создание HTML-страницы, выводящей список записей
  2. Обработка PUT-запроса на PHP
  3. Выполнение PUT-запроса в браузере
  4. Заключение
  5. Комментарии

В этой статье мы рассмотрим пример обработки запроса на PHP, отправленного клиентом с использованием HTTP-метода PUT. Отправка данных с помощью данного метода используется в REST API и сопоставляется в этой архитектуре с операцией обновления объекта (ресурса).

Создание HTML-страницы, выводящей список записей

Для примера необходимо иметь установленный локальный веб-сервер с поддержкой PHP. Его можно установить на WSL, собрать в Docker или использовать уже готовые инструменты. Например: OpenServer, XAMPP или какие-то другие.

Перед тем как переходить к написанию кода для обновления данных посредством HTTP-запроса PUT, сначала разработаем HTML-страницу. На ней будем выводить список записей из json-файла в табличном виде.

Для этого создадим новый файл index.html:

HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Список смартфонов</title>
  </head>
  <body>
    <h1 class="h4 my-3">Список записей</h1>
  </body>
</html>

После этого вставим в <body> HTML-таблицу:

HTML
<table class="table table-bordered mb-5">
  <thead>
    <tr>
      <th scope="col" style="width: 5%">#</th>
      <th scope="col" style="width: 55%">Модель смартфона</th>
      <th scope="col" style="width: 20%">Цена</th>
      <th scope="col" style="width: 20%"></th>
    </tr>
  </thead>
  <tbody></tbody>
</table>

В качестве оформления будем использовать CSS-фреймворк Bootstrap 5. Чтобы его подключить к странице можно добавить в HTML-документ следующие 2 строчки кода:

HTML
<!-- CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>

Если открыть страницу, то на текущем моменте она будет выглядеть следующим образом:

Создание основной HTML-страницы, содержащей Bootstrap таблицу

Формировать тело таблицы, содержащей записи, будем с помощью JavaScript. Для этого создадим файл main.js и подключим его к странице:

HTML
<script src="main.js"></script>

Сами данные будем получать методом fetch() из файла data.json:

JavaScript
const escapeHTML = (html) => String(html).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll("'", '&#39;').replaceAll('"', '&quot;');

const template = (row) => `<tr data-id="${escapeHTML(row.id)}">
    <th scope="row">${escapeHTML(row.id)}</th>
    <td><input type="text" name="title" class="form-control" value="${escapeHTML(row.title)}"><div class="invalid-feedback"></div></td>
    <td><input type="text" name="price" class="form-control" value="${escapeHTML(row.price)}"><div class="invalid-feedback"></div></td>
    <td><button type="button" class="btn btn-warning w-100">
      <span class="spinner-border spinner-border-sm d-none" aria-hidden="true"></span>
      <span role="status">Обновить</span>
    </button></td>
  </tr>`;

document.addEventListener('DOMContentLoaded', async () => {
  const response = await fetch('data.json', {
    method: 'GET',
    headers: {
      'pragma': 'no-cache',
      'cache-control': 'no-cache'
    }
  });
  if (response.ok) {
    const phones = await response.json();
    const html = phones.map((row) => template(row)).join('');
    document.querySelector('tbody').innerHTML = html;
  }
});

Для формирования HTML-кода одной записи <tr> будем использовать функцию template(). Запрос будем выполнять посредством метода fetch() после того, как DOM-дерево страницы будет готово. Для этого воспользуемся событием DOMContentLoaded. Также, чтобы браузер не кэшировал наш запрос, передадим в метод fetch() необходимые заголовки:

JavaScript
{
  'pragma': 'no-cache',
  'cache-control': 'no-cache'
}

После получения успешного ответа от сервера воспользуемся методом map(). С помощью него сформируем массив HTML-строк для нашей таблицы. Далее применим метод join(), посредством которого объединим все элементы этого массива в строку:

JavaScript
const html = phones.map((row) => template(row)).join('');

Вставку на страницу полученного HTML выполним с помощью свойства innerHTML:

JavaScript
document.querySelector('tbody').innerHTML = html;

В результате страница index.html будет выводиться следующим образом:

HTML-страница, содержащая список записей в табличном виде

Обработка PUT-запроса на PHP

На этом шаге напишем код для обновления строки записи на сервере, которые в рамках этой задачи у нас для простоты находятся в файле data.json:

JSON
[
  {
    "id": 1,
    "title": "6.8" Смартфон Samsung Galaxy S24 Ultra 1024 ГБ",
    "price": 189999
  },
  {
    "id": 2,
    "title": "6.7" Смартфон Google Pixel 8 Pro 256",
    "price": 159999
  },
  {
    "id": 3,
    "title": "6.73" Смартфон Xiaomi 14 Ultra 512 ГБ",
    "price": 139999
  }
]

Запрос на обновление определённой записи будем получать от клиента посредством метода PUT. Сами данные, в соответствии с которыми нам необходимо выполнить эту операцию, будем брать из тела запроса, в котором они должны быть представлены в формате JSON-строки. Чтение необработанных данных из тела запроса выполним посредством потока php://input:

PHP
$input = file_get_contents('php://input');

php://input можно использовать для чтения тела любого запроса, кроме POST с enctype="multipart/form-data" при включенной опции enable_post_data_reading.

После этого действия у нас в переменной $input будет содержаться данные в формате JSON, которые мы направили серверу с клиента. Теперь декодируем строку в PHP-значение с помощью функции json_decode():

PHP
$input = json_decode($input, true);

Сейчас в $input у нас будет содержаться ассоциативный массив. Сохраним в отдельные переменные ключ записи (id), название модели смартфона (title) и его цену (price).

PHP
$id = $input['id']; // ключ записи (id)
$title = $input['title']; // название модели смартфона
$price = $input['price']; // цена

Далее прочитаем всё содержимое файла data.json с помощью file_get_contents() и преобразуем его в ассоциативный массив, который сохраним в переменную $phones:

PHP
$phones = json_decode(file_get_contents('data.json'), true);

Чтобы обновить данные определённой записи в этом массиве нам необходимо знать её числовой индекс. Что его найти выполним перебор массива посредством цикла foreach:

PHP
$phoneIndex = false;
foreach ($phones as $index => $phone) {
  if ($phone['id'] === $id) {
    $phoneIndex = $index;
    break;
  }
}

Так как отправлять ответ клиенту будем в формате JSON, то создадим ассоциативный массив с ключами data и success:

PHP
$result = [
  'data' => [],
  'success' => false,
];

Ключи будут содержать следующую информацию:

  • success – результат успешности обновления записи в файле data.json;
  • data – данные обновлённой записи.

После этого обновим данные в $phones на те, которые мы получили от веб-браузера и сохраним их в data.json с помощью функции file_put_contents():

PHP
if ($phoneIndex !== false) {
  $phones[$phoneIndex]['title'] = $title;
  $phones[$phoneIndex]['price'] = $price;

  if (file_put_contents('data.json', json_encode($phones))) {
    $result['data'] = $phones[$phoneIndex];
    $result['success'] = true;
  }
}
  
echo json_encode($result);

В конце этого кода посредством языковой конструкции echo будет выводиться строка, которая в формате JSON отправится клиенту (веб-браузеру).

Выполнение PUT-запроса в браузере

На данном этапе вернёмся обратно на клиент. С помощью JavaScript напишем следующий код, который будет выполняться при нажатии на кнопку «Обновить»:

JavaScript
document.addEventListener('click', async (e) => {
  const submit = e.target.closest('.js-btn-update');
  if (!submit || submit.disabled) {
    return;
  }
  e.preventDefault();
  submit.disabled = true;
  const tr = submit.closest('tr');
  const body = {
    id: Number(tr.dataset.id),
    title: tr.querySelector('[name="title"]').value,
    price: Number(tr.querySelector('[name="price"]').value),
  }
  const response = await fetch('/phones/update.php', {
    method: 'put',
    headers: {
      "X-Requested-With": "XMLHttpRequest",
    },
    body: JSON.stringify(body),
  });
  if (response.ok) {
    // действия в зависимости от результата, полученного от сервера
  }
  submit.disabled = false;  
});

Здесь в начале кода мы проверяем, действительно ли пользователь нажал на кнопку «Обновить». Если нет или кнопка в данный момент не активна, то завершаем выполнение:

JavaScript
const submit = e.target.closest('.js-btn-update');
if (!submit || submit.disabled) {
  return;
}

Далее мы переводим кнопку в неактивное состояние и отменим её поведение по умолчанию посредством вызова метода preventDefault():

PHP
submit.disabled = true;
e.preventDefault();

После этого с помощью метода closest() получаем ближайший родительский элемент <tr>, то есть строку таблицы:

JavaScript
const tr = submit.closest('tr');

Затем создадим объект, используя литеральный синтаксис:

JavaScript
const data = {
  id: Number(tr.dataset.id),
  title: tr.querySelector('[name="title"]').value,
  price: Number(tr.querySelector('[name="price"]').value),
}

Этот объект содержит 3 свойства: id, title и price. Значение id мы получим из data-атрибута id, а title и price – из значений соответствующих input-элементов.

Теперь отправим PUT-запрос на сервер:

JavaScript
const response = await fetch('/phones/update.php', {
  method: 'put',
  headers: {
    'Content-Type': 'application/json;charset=utf-8',
    'X-Requested-With': 'XMLHttpRequest'
  },
  body: JSON.stringify(data),
});

Где:

  • /phones/update.php – URL, на который необходимо отправить запрос;
  • method – HTTP-метод, в данном случае PUT;
  • headers – заголовки запроса (с помощью Content-Type будем «говорить» серверу, что посылаем JSON, а посредством X-Requested-With – что это AJAX-запрос);
  • body – тело запроса, будет содержать объект data как JSON.

Так как получение ответа от сервера занимает некоторое время, то с помощью ключевого слова await мы заставляем интерпретатор JavaScript ждать до тех пор, пока fetch не выполнится.

С помощью свойства ok проверяем находится ли код HTTP-статуса в диапазоне 200-299:

JavaScript
if (response.ok) {
  // ...
}

Это свойство возвращает логическое значение: true или false. Для чтения ответа в формате JSON используем метод json():

JavaScript
const result = await response.json();

Переменная result будет содержать объект со свойствами: success и data. Свойство success будет содержать логическое значение, которое будет говорить нам, получилось ли на сервере обновить запись. То есть от результата success, например, можно выводить то или иное сообщение:

JavaScript
if (result.success) {
  showToast(result.success, `Данные строки #${tr.dataset.id} успешно обновлены!`);
} else {
  showToast(result.success, `Ошибка при обновлении данных в строке #${tr.dataset.id}!`);
}

Вывод сообщений будем осуществлять с помощью компонента Bootstrap Toast. Для этого на страницу добавим контейнер и функцию в код JavaScript:

JavaScript
<div class="toast-container position-fixed top-0 end-0 p-3" style="max-width: 16rem;"></div>

<script>
const toastHTML = (success, message) => `<div class="toast align-items-center text-bg-${success ? 'success' : 'danger'} border-0" role="alert" aria-live="assertive" aria-atomic="true">
  <div class="d-flex">
    <div class="toast-body">${message}</div>
      <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
    </div>
  </div>`;
<script>

Заключение

В завершении необходимо отметить, что мы создали только базовый код. В реальности в него нужно внести различные проверки, особенно в части тех данных, что нам прислал клиент.

Например, проверить, что запрос отправлен с помощью AJAX и методом PUT:

PHP
$result = [
  'data' => [],
  'errors' => [],
  'message' => '',
  'success' => false,
];

$requestWith = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? '';
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? '';

if ($requestWith != 'XMLHttpRequest' || $requestMethod != 'PUT') {
  echo json_encode($result);
  exit();
}

Что в JSON содержатся все необходимые данные и они являются корректными:

PHP
if (isset($input['id']) && is_int($input['id'])) {
  $id = $input['id'];
} else {
  $result['message'] = 'Произошла ошибка при обновлении записи. Идентификатор не был передан или он не является числом.';
}

if (isset($input['title']) && mb_strlen($input['title']) > 5) {
  $title = $input['title'];
} else {
  $result['errors']['title'] = 'Поле "Модель смартфона" должна быть больше 5 символов.';
}

if (isset($input['price']) && (is_int($input['price']) || is_double($input['price']))) {
  $price = $input['price'];
} else {
  $result['errors']['price'] = 'Поле "Цена" не является числом.';
}
  
if ($result['message'] || count($result['errors'])) {
  echo json_encode($result);
  exit();
}

На клиенте показать все найденные ошибки:

JavaScript
if (response.ok) {
  const result = await response.json();
  if (result.success) {
    tr.querySelectorAll('input').forEach((input) => {
      input.classList.remove('is-invalid');
    });
    showToast(result.success, `Строка #${Number(tr.dataset.id)} успешно обновлена!`);
  } else {
    showToast(result.success, `Ошибка при обновлении строки #${Number(tr.dataset.id)}!`);
    Object.keys(result.errors).forEach((key) => {
      const el = tr.querySelector(`[name="${key}"]`);
      el.classList.add('is-invalid');
      showToast(result.success, result.errors[key]);
    });
  }
}

Комментарии: 0