Skip to main content
Being Idea Innovations
Submitting a Form to PHP with JavaScript (AngularJS Is End-of-Life)
Back to Blog

Submitting a Form to PHP with JavaScript (AngularJS Is End-of-Life)

16 September 20185 min readPHP
Share:

Before the code, the thing you need to know: AngularJS (the 1.x line) reached end of life on 31 December 2021. Google no longer ships security patches, and it will never receive fixes for vulnerabilities discovered from now on.

If you are starting something new, do not use it. If you are maintaining an existing app, the migration notes at the end are the useful part. Either way, the underlying task — submit a form, save it in MySQL, render the list without a page reload — is worth doing properly.

Why not just update the old code?

The AngularJS example that circulates for this, including the earlier version of this article, is broken on any supported version even setting EOL aside:

// Removed in Angular 1.6
$http.get("index.php?action=getComments")
  .success(function (data) { $scope.comments = data; });

// Removed in Angular 1.7 — global controller functions no longer resolve
function commentsController($scope, $http) { }

.success() and .error() were deprecated in 1.4 and removed in 1.6 in favour of standard promise .then(). Implicit global controller functions stopped working in 1.7 — controllers must be registered on a module. So the code fails on the last three releases of a framework that itself is unsupported.

There is also a straightforward injection bug in it:

data : "action=add&msg=" + comment.msg + "&name=" + comment.name

Any & or = a user types corrupts the request body. A comment saying "profit & loss" silently arrives truncated.

The modern version

No framework needed for a form and a list.

Markup

<form id="comment-form" class="comment-form">
  <input type="hidden" name="csrf" value="<?= htmlspecialchars($csrf) ?>" />

  <div class="field">
    <label for="name">Name</label>
    <input type="text" id="name" name="name" required maxlength="80" />
  </div>

  <div class="field">
    <label for="message">Message</label>
    <textarea id="message" name="message" required maxlength="2000"></textarea>
  </div>

  <button type="submit">Post comment</button>
  <p id="form-status" role="status" aria-live="polite"></p>
</form>

<ul id="comments" class="comments"></ul>

The JavaScript

const form    = document.getElementById('comment-form');
const list    = document.getElementById('comments');
const status  = document.getElementById('form-status');

async function loadComments() {
  try {
    const res = await fetch('/api/comments.php', { headers: { Accept: 'application/json' } });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    render(await res.json());
  } catch {
    status.textContent = 'Could not load comments.';
  }
}

function render(comments) {
  list.replaceChildren();
  for (const comment of comments) list.prepend(buildItem(comment));
}

function buildItem(comment) {
  const li = document.createElement('li');
  li.className = 'comment';
  li.dataset.id = comment.id;

  const name = document.createElement('strong');
  name.textContent = comment.name;          // textContent — no XSS possible

  const time = document.createElement('time');
  time.dateTime = comment.created_at;
  time.textContent = new Date(comment.created_at).toLocaleString();

  const body = document.createElement('p');
  body.textContent = comment.message;

  li.append(name, time, body);
  return li;
}

form.addEventListener('submit', async (e) => {
  e.preventDefault();

  const button = form.querySelector('button');
  button.disabled = true;                    // prevents double submission
  status.textContent = 'Posting…';

  try {
    const res = await fetch('/api/comments.php', {
      method: 'POST',
      // FormData encodes correctly — no manual string concatenation.
      body: new FormData(form),
      headers: { Accept: 'application/json' },
    });

    const data = await res.json();

    if (!res.ok) {
      status.textContent = data.error ?? 'Could not post your comment.';
      return;
    }

    list.prepend(buildItem(data));
    form.reset();
    status.textContent = 'Comment posted.';
  } catch {
    status.textContent = 'Network error. Please try again.';
  } finally {
    button.disabled = false;
  }
});

loadComments();

Two things doing real work here. FormData handles encoding, so ampersands and equals signs in user input cannot corrupt the request. And textContent rather than innerHTML means a comment containing <script> renders as literal text — the XSS hole in the original disappears by construction.

The PHP API

<?php
declare(strict_types=1);

session_start();
header('Content-Type: application/json');

$pdo = new PDO('mysql:host=localhost;dbname=demo;charset=utf8mb4', $user, $pass, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

if ($_SERVER['REQUEST_METHOD'] === 'GET') {
    $stmt = $pdo->query(
        'SELECT id, name, message, created_at FROM comments ORDER BY created_at DESC LIMIT 50'
    );
    echo json_encode($stmt->fetchAll(), JSON_THROW_ON_ERROR);
    exit;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
        http_response_code(403);
        exit(json_encode(['error' => 'Session expired. Please refresh.']));
    }

    $name    = trim($_POST['name'] ?? '');
    $message = trim($_POST['message'] ?? '');

    if ($name === '' || mb_strlen($name) > 80) {
        http_response_code(422);
        exit(json_encode(['error' => 'Please enter a name under 80 characters.']));
    }
    if ($message === '' || mb_strlen($message) > 2000) {
        http_response_code(422);
        exit(json_encode(['error' => 'Please enter a message under 2000 characters.']));
    }

    $stmt = $pdo->prepare(
        'INSERT INTO comments (name, message, created_at) VALUES (?, ?, NOW())'
    );
    $stmt->execute([$name, $message]);

    $id = (int) $pdo->lastInsertId();

    // Return the created row so the client can render it without a refetch.
    http_response_code(201);
    echo json_encode([
        'id'         => $id,
        'name'       => $name,
        'message'    => $message,
        'created_at' => date('c'),
    ], JSON_THROW_ON_ERROR);
    exit;
}

http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);

The schema

CREATE TABLE comments (
  id         BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  name       VARCHAR(80)  NOT NULL,
  message    TEXT         NOT NULL,
  created_at DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  KEY idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

Two corrections to the original schema. It used CHARSET=latin1, which cannot store emoji or most non-Western scripts — utf8mb4 is the only sensible choice. And its column was named comments while the JavaScript sent msg, so the two never matched.

Deleting

The original deleted via GET:

// Wrong — GET must not change state
url : "index.php?action=delete&id=" + id

A crawler following links, or a browser prefetching, will delete your data. Worse, any site can trigger it with <img src="…action=delete&id=5">. Use DELETE or POST, with a CSRF token and an authorisation check:

if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
    parse_str(file_get_contents('php://input'), $input);

    if (!hash_equals($_SESSION['csrf'] ?? '', $input['csrf'] ?? '')) {
        http_response_code(403);
        exit(json_encode(['error' => 'Invalid token']));
    }

    if (!currentUserCanModerate()) {
        http_response_code(403);
        exit(json_encode(['error' => 'Not permitted']));
    }

    $pdo->prepare('DELETE FROM comments WHERE id = ?')->execute([(int) $input['id']]);
    http_response_code(204);
    exit;
}

If you are maintaining an AngularJS app

You have three realistic options:

OptionEffortWhen it fits
Rewrite in a current frameworkHighActively developed products
Incremental migrationMedium, ongoingLarge apps you cannot pause
Extended support vendorPaidBuying time only

For the incremental path, replace one route or component at a time with plain JavaScript or your target framework, keeping both on the page during the transition. Start with the pieces that touch user input or authentication, since those carry the security risk.

If you must patch existing AngularJS code, at minimum modernise the promise handling:

// Old — removed in 1.6
$http.get(url).success(fn);

// Works on 1.6+
$http.get(url).then(function (response) {
  $scope.comments = response.data;   // note: .data, not the raw response
});

// And register controllers explicitly — globals stopped working in 1.7
angular.module('app', []).controller('CommentsController', ['$scope', '$http', function ($scope, $http) {
  // …
}]);

Summary

  • AngularJS is end-of-life and unpatched — do not start new work with it.
  • FormData for encoding; never concatenate request bodies by hand.
  • textContent, not innerHTML, for user-supplied data.
  • Prepared statements and server-side validation regardless of the front end.
  • CSRF tokens on every state-changing request.
  • Never mutate state from a GET.
  • utf8mb4, not latin1.
  • Return the created resource so the client need not refetch.
Share:

Written by

Amit Verma

Founder / Senior Software Engineer

Amit leads engineering at Being Idea. With 15+ years building scalable software products across global markets, he drives architecture decisions and engineering culture across every engagement.

More articles by Amit

Want to talk tech?

We ship software that scales. Let's work together.

No long-term contracts
Senior engineers only
US · AU · NZ timezone coverage
14-day trial on retainers