english

learn php

18. ajax post
14 minutes / 2774 words

eighteen

ajax post

get is great if all you want is to retrieve something but, of course, most of the time, what you need to do is send data. thankfully, ajax also supports post. it takes a little more javascript knowledge but it's still very similar to php command structure so it should look familiar and manageable.

we've talked about logins so we'll use that as a post example in combination with some of the php skills you already know. this can be done with a database but we'll use json files for simplicity.

a short note on security. this example includes files that should never be showed to the user so it's important to ensure any system built in this way doesn't allow users to see them. that's not difficult to accomplish but it's an important step.

json/users.json

[
    {
        "id": "f3a9b570-0898-428a-a11e-2da006cb47d4",
        "name": "Sharon Wu",
        "passwordHash": "$2a$12$5zMgLwaKurwDAHmCQdXnJO/5w6zMDzwrwypZQQ2Z9HYNalj1H9ydm",
        "active": 1,
        "createdDatetime": 1578250351,
        "email": "[email protected]"
    },
    {
        "id": "03fd55f1-616e-4d5e-ba9b-2b520da73eb0",
        "name": "Lori Kim",
        "passwordHash": "$2a$12$9I3p6S4QZMXInjqe1U4S.O94sUrOsOSiTirJJJPcLcIOb/ADuHXDi",
        "active": 1,
        "createdDatetime": 1467646375,
        "email": "[email protected]"
    },
    {
        "id": "2c4b7ce3-5c75-4eff-b2e3-67ed47712d08",
        "name": "Disha Wish",
        "passwordHash": "$2a$12$YVz68eK3zsC463xNKFNQoeBpYjCJKjLg/ob7jBxX7jVeZmCeGt296",
        "active": 1,
        "createdDatetime": 1521929794,
        "email": "[email protected]"
    },
    {
        "id": "b5e3a84c-c8ea-4239-8208-132046c18caf",
        "name": "Emily Barnes",
        "passwordHash": "$2a$12$0.JodjOfIYMq1FLbbPbD2.pz/Xo4J5EobqZBNZuauvcWBGu1/SFXe",
        "active": 1,
        "createdDatetime": 1500984495,
        "email": "[email protected]"
    },
    {
        "id": "5779e2c8-328d-40c5-a1db-72deed688a34",
        "name": "Evelyn Silk",
        "passwordHash": "$2a$12$vmxowUCxREe8vzurgd1XTufeqnymkcJMOs2CfrTVVz5V86bMQHpeu",
        "active": 1,
        "createdDatetime": 1509992042,
        "email": "[email protected]"
    },
    {
        "id": "664ae5db-6228-44cf-9ed7-6e0d3bea4186",
        "name": "Ava Machs",
        "passwordHash": "$2a$12$BSjTwROtQxw4YJLgvt3d0u3yh7LkqV4UmnufGYGwU5Z2iswnZxUAy",
        "active": 1,
        "createdDatetime": 1551923573,
        "email": "[email protected]"
    },
    {
        "id": "ee14df7c-49ea-46bd-b407-87ecc5e0fbb7",
        "name": "Harper Wren",
        "passwordHash": "$2a$12$GAJzAKY5K.snsEuhtE51NupWfk1yz.SVncRLvfpBBkm42bLdb9M4O",
        "active": 1,
        "createdDatetime": 1468642502,
        "email": "[email protected]"
    },
    {
        "id": "bb9d0d36-0a58-464e-a29f-abfe47f2bc4c",
        "name": "Mia Lynx",
        "passwordHash": "$2a$12$i9cYFyBKEXFoJQqtxJn.PONoK5W4Q31ameBoAbEJq.4f0YhTzVyhC",
        "active": 0,
        "createdDatetime": 1479441638,
        "email": "[email protected]"
    },
    {
        "id": "5c54435e-56b8-4a13-8247-ee457e93388a",
        "name": "Sophie Koh",
        "passwordHash": "$2a$12$mO4GKM7HV25FLDqGKGP4OOJhBj30mqUCR19RPmkB.eI.3i0PtdkbG",
        "active": 1,
        "createdDatetime": 1489922588,
        "email": "[email protected]"
    },
    {
        "id": "45d625ae-f79a-4924-90c2-b0fae01a55cb",
        "name": "Isla White",
        "passwordHash": "$2a$12$qgCuY.J.Uk5B22vhjlf94e82snxY.Vk/AuFnGaWocsjZq8y7gc98y",
        "active": 1,
        "createdDatetime": 1538915785,
        "email": "[email protected]"
    }
]

this is a json representation of some of the same information from the user database. some data has been left out for simplicity.

index.htm

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=10.0" />
        <script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
        <script src="js/login.js"></script>
        <title>ajax demonstration</title>
        <base href="http://localhost" />
    </head>
    <body>
        <main>
            <h1>login</h1>
            <section id="target">
                <ask id="askLogin">
                    <input type="text" id="email" placeholder="email" />
                    <input type="password" id="password" placeholder="password" />
                    <button id="login">login</button>
                </ask>
            </section>
        </main>
    </body>
</html>

js/login.js

$(window).on("load", function () {
    $(document).on("click", "ask button", function () {
        submitAsk($(this).closest("ask"), $(this));
    });
});

function display(c) {
    c.trim().length > 0 && $("#target").html(c);
}

function submitAsk(ask, button) {
    askData = new FormData();
    ask.find("input,select,textarea").each(function () {
        askData.append($(this).attr("id"), $(this).val());
    });

    buildPath = "php/login.php";
    button.attr("id") && (buildPath += "?action=" + button.attr("id"));

    $.post(buildPath, askData)
        .done(function (r) {
            display(r);
        });
}

php/login.php

<?php

session_id() === "" && session_start();

$json = __DIR__ . "/../json/";
$action = isset($_GET["action"]) ? trim($_GET["action"]) : "";

$action == "login" && doLogin();

showLogin();

function doLogin()
{
    session_destroy() && session_start();
    if (!isset($_POST["email"]) || !isset($_POST["password"])) {
        return false;
    }
    $email = trim($_POST["email"]);
    $password = trim($_POST["password"]);
    if (!($user = getUser($email)) || $user["active"] != 1 || !password_verify($password, $user["passwordHash"])) {
        return false;
    }
    $_SESSION["user"] = $user;
}

function getUser($email)
{
    $users = readJson("users");
    return ($user = array_search($email, array_column($users, "email"))) === false ? false : $users[$user];
}

function readJson($f)
{
    global $json;
    !str_starts_with($f, "/") && ($f = $json . $f);
    !str_ends_with($f, ".json") && ($f .= ".json");
    $j = getFile($f);
    $j = json_validate($j) ? json_decode($j, true) : [];
    return isset($j) && count($j) ? $j : [];
}

function getFile($f)
{
    $c = is_file($f) && is_readable($f) ? file_get_contents($f) : "";
    return is_string($c) ? $c : "";
}

function showLogin()
{
    isset($_SESSION["user"]) ? print_r("hi, " . $_SESSION["user"]["name"] . "!") : print_r("go away!");
}

before continuing with the detailed explanation, take a minute to try to figure it out on your own. there should only be a few lines you haven't already seen.

beginning with index.htm, there's very little that's new and it's all things we've seen before in principle. two input tags, one for email, the other password. the button is no different from the one in the get example. we have a section with an id to give us a specific part of the page to update using the javascript and an ask to wrap all the active elements so we can iterate through them. remember, nothing in the html performs any actions. it's just a static collection of pieces within a document.

login.js is where most of the new code appears. we'll start with the first function, which should look familiar because it's similar to the get example's first function. in fact, it is almost guaranteed a function like this will start any page using the jquery framework because this is how actions are initialized -- in particular, what happens when things are clicked.

in our second on function, we have ask button. in stylesheets and jquery searches, names are given using their hierarchy. this references a button inside an ask. remember that names without prefixes are tags, those beginning with . are classes and those beginning with # are ids. note that this applies to every button inside every ask. the fact that there's only one in this example doesn't change how the code would behave on an html document with many. they would all trigger the same action.

they call submitAsk with two parameters. we already know what the second is -- $(this) is the element that was clicked to make the function call happen. closest does a search up the hierarchical chain to find the first element that matches. in this case, it's looking for ask, a reference to a tag because it has no prefix. so the first parameter is pointing to <button id="login">, the second to <ask id="askLogin">. it's best to write it in this generic form so it works with every button in every ask from just one function.

we create a new empty FormData with the variable name askData. this is similar to a generic array with some special quirks for use when sending form data. ask is the first parameter so we already know it's referring to the html block with the input tags inside. find looks for any input, select or textarea inside that block and each applies its function to each one found in order. that function calls append, adding each to askData. the first parameter is the element's id and the second is its value. what we've done so far, then, is to build a FormData containing the information from all the places where the user can set a value -- in this case, their email and password.

next, we have to figure out where to send the post. we're calling the only php file in the example, login.php, which is the initial contents of the variable buildPath. we then check for an id in the button, the second parameter in the function, the button that was clicked to start the action. if it has an id, we add ?action= and that id to buildPath. if not, it just stays with only the php file. because + adds strings in javascript, += functions like .= in php.

the next piece should look very similar to the get function from the previous example. instead of just calling the address, though, we call the address, buildPath, and the payload, askData, that we just filled with the input data. when that returns content, it is passed to a function that calls display with that content.

display should look very familiar because it contains the same code you've already seen for html target replacement. it checks to make sure the content actually has something inside and, if it does, makes that the contents of #target in the html.

all we have left to look at is the php code. we've talked about session creation and destruction already. the first line checks to see if a session already exists. session_id returns the unique identifier of the current session. if it's blank, there's no session. so we start the session by calling session_start. session_start can technically be called if there's already an active session but it's a good practice to only call it once and save the extra overhead.

next, we set a variable pointing to the directory where our json information is stored. because we're calling a php file inside the php directory instead of in the main directory, we have to go up one directory level by using ../ before diving into the json directory. it's important to be careful of where to add a /. __DIR__ doesn't end in / so we have to add one before any files or directories are appended. we also add one to the end of $json because we're going to add a filename to it later.

we then check whether there was a querystring variable called action set and, if it was, use that as the value for $action. if there wasn't, $action is blank.

the next piece is just one of what would be multiple options in a real-world example. if $action is login, we call doLogin. this is the kind of call that would go down through a list of possible actions and perform tasks based on what was requested.

once that list of possible actions is done, we call the only function that writes anything to the browser, showLogin. first, though, let's take a look at the functions in the order they're called.

doLogin starts, because it's a login function, by making sure there's no current session -- in other words, making sure nobody's already logged in -- and starting a blank one. it then checks to see if either email or password in the post request is blank and, if so, returns false, realistically stopping the login process without needing to check anything deeper. if the user hasn't provided an email and password, why even load user information to check against?

we can now assume there's a username and password to compare so we can set our internal variables, $email and $password to the ones with the same names from the post request and trim any leading or ending whitespace. this is important, especially with login information, as people often paste this in and, if they've selected new lines or spaces when they've copied them, there's no reason to punish them for that when it doesn't change the meaningful content they've entered. this was actually an ongoing annoyance for gmail and hotmail users in the early days of free webmail platforms.

the next line is the one that does most of the work in this function. we check to see if any of three things is false and return false if that's the case. first, we call getUser, which returns a user matching the $email passed in if one exists in the json. we'll look at how that works in a moment. if such a user is returned, we then check to see if the active value of that user is anything other than 1. if it's not, we check to make sure the value of $password matches the passwordHash value in the returned user using password_verify, which we looked at when we talked about password hashing.

assuming we've successfully navigated all those checks, we set the variable user in the session to be the user that was returned by getUser. note that, at this point, $user only exists inside this function and, if it hasn't matched both being active and having the right password, the variable is cleared when the function ends so we don't have to return login failure anywhere. the session where we store the logged-in user never contains any user information unless everything is successful and we get to the last line of this function where that's added.

getUser takes the $email variable that's passed in. first, we call readJson for users and set $users to whatever it returns. this is the complete user file. what we're looking for is whichever user in $users matches $email, if one exists. array_search searches for a value (the first parameter) inside an array (the second parameter). in the case of a nested array, we use array_column to specify which array and nested array's key to match. array_search, returns the key so, if it's not identical to false, we set $user to be the one user in $users with that key. otherwise, we return false. we have to check to make sure it's not identical rather than just equivalent because 0 is a valid key -- typically, the first element in an array. if we checked for equivalent to false, a perfectly normal response, the first element in a numbered array, would be treated as a failure condition.

readJson takes the filename, $f, the placeholder we've already used several times. first, we read global variable $json, which we set at the beginning of the file. then we check to see if $f starts with a / using str_starts_with. if it does, it's not just the filename but a whole file path. if it's just the filename, we add the path to the beginning. we do the same thing to the end with str_ends_with to see if the filename has a .json extension already. if it doesn't, we add it. with that completed, we now call getFile on that full filename with path and set its contents to $j.

with those contents, we make sure it's valid json using json_validate and, if it is, convert it to a php array using json_decode as we have several times before, saving that back to $j, replacing the raw data. if it's not valid json, we set $j to an empty array. if $j has content, we return it. if it doesn't, we return an empty array. this ensures readJson always returns an array, never false or null, meaning we don't have to check what kind of information comes back when we call the function.

getFile takes a filename, checks to make sure it exists as a file and is accessible then loads the file using file_get_contents. if all this is true and the file has data, it sets that to $c. if not, $c becomes an empty string. next, we check to make sure $c is a valid string and return it. if it's not, we just return empty string. this ensures only a valid string is ever returned, not false or null.

the only piece left is our display function, showLogin. this just checks to see if the session contains a user. this doesn't depend on the login function having just been run, only that the session has a user from either this time through or a previous process. if there's a user in the session, we write hi and the user's name. if there isn't, we write go away!. of course, in a real-world situation, we would actually do something with the user or provide a way to login. for our purposes, this is all we need to understand how ajax post works because we've already looked at all the rest of the php actions that can be done with the data once it's inside the program.

assignments

© avi sato. creative commons attribution-noncommercial-noderivatives.