This article was peer reviewed by Wern Ancheta. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!
I’m always yammering on about writing asynchronous PHP code, and for a reason. I think it’s healthy to get fresh perspectives – to be exposed to new programming paradigms.
Asynchronous architecture is common in other programming languages, but it’s only just finding its feet in PHP. The trouble is that this new architecture comes with a cost.
I don’t talk about that cost enough.
When I recommend frameworks like Icicle, ReactPHP, and AMPHP, the obvious place to start with them is to create something new. If you have an existing site (perhaps running through Apache or Nginx), adding daemonised PHP services to your app is probably not as easy as just starting over.
It takes a lot of work to integrate new, asynchronous features into existing applications. Often there are good reasons and great benefits, but a rewrite is always a hard-sell. Perhaps you can get some of the benefits of parallel execution without using an event loop. Perhaps you can get some of the benefits of web sockets without a new architecture.
I’m going to show you a Sockets-as-a-Service service, called Socketize. Try saying that a few times, out loud…
Note: Web sockets involve a fair amount of JavaScript. Fortunately, we don’t need to set up any complicated build chains. You can find the example code for this tutorial here.
Key Takeaways
- Asynchronous architecture is becoming more common in PHP, but its integration into existing applications can be challenging and may require a complete rewrite.
- The Socketize service allows for the benefits of web sockets without the need for a new architecture. It allows for easy integration of new, asynchronous features into existing applications.
- Web sockets can create persistent connections between browsers and servers, which can push new data from the server and avoid continuous polling. However, PHP scripts are not designed to hold many connections open at once, which can be inefficient.
- Socketize can solve the issue of inefficiency in PHP scripts by allowing for real-time, bidirectional communication between a client and a server. This makes it ideal for applications that require real-time updates.
- Using Socketize, developers can push events to the browser every time a database record is saved, connect gamers to each other, and alert conference speakers when they are close to attendees with questions, all within a traditional PHP codebase.
Setup!
Let’s set up a simple CRUD example. Download the SQL script, and import it to a local database. Then let’s create a JSON endpoint:
$action = "/get";
$actions = ["/get"];
if (isset($_SERVER["PATH_INFO"])) {
if (in_array($_SERVER["PATH_INFO"], $actions)) {
$action = $_SERVER["PATH_INFO"];
}
}
$db = new PDO(
"mysql:host=localhost;dbname=[your_database_name];charset=utf8",
"[your_database_username]",
"[your_database_password]"
);
function respond($data) {
header("Content-type: application/json");
print json_encode($data) and exit;
}
This code will decide whether a request is being made against a valid endpoint (currently only supporting /get
). We also establish a connection to the database, and define a method for allowing us to respond to the browser with minimal effort. Then we need to define the endpoint for /get
:
if ($action == "/get") {
$statement = $db->prepare("SELECT * FROM cards");
$statement->execute();
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
$rows = array_map(function($row) {
return [
"id" => (int) $row["id"],
"name" => (string) $row["name"],
];
}, $rows);
respond($rows);
}
/get
is the default action. This is the thing we want this PHP script to do, if nothing else appears to be required. This will actually become more useful as we add functionality. We also define a list of supported actions. You can think of this as a whitelist, to protect the script against strange requests.
Then, we check the PATH_INFO
server property. We’ll see (in a bit) where this info comes from. Just imagine it contains the URL path. So, for http://localhost:8000/foo/bar, this variable will contain /foo/bar
. If it’s set, and in the array of allowed actions, we override the default action.
Then we define a function for responding to the browser. It’ll set the appropriate JSON content type header, and print a JSON-encoded response, which we can use in the browser.
Then we connect to the database. Your database details will probably be different. Pay close attention to the name of your database, your database username and database password. The rest should be fine as-is.
For the /get
action, we fetch all rows from the cards
table. Then, for each card, we cast the columns to the appropriate data type (using the array_map
function). Finally, we pass these to the respond
function.
To run this, and have the appropriate PATH_INFO
data, we can use the built-in PHP development server:
$ php -S localhost:8000 server.php
The built-in PHP development server is not good for production applications. Use a proper web server. It’s convenient to use it here, because we don’t need to set up a production server on our local machine to be able to test this code.
At this point, you should be able to see the following in a browser:
If you’re not seeing this, you probably should try to resolve that before continuing. You’re welcome to use alternative database structures or data, but you’ll need to be careful with the rest of the examples.
Now, let’s make a client for this data:
<!doctype html>
<html lang="en">
<head>
<title>Graterock</title>
</head>
<body>
<ol class="cards"></ol>
<script type="application/javascript">
fetch("http://localhost:8000/get")
.then(function(response) {
return response.json();
})
.then(function(json) {
var cards = document.querySelector(".cards");
for (var i = 0; i < json.length; i++) {
var card = document.createElement("li");
card.innerHTML = json[i].name;
cards.appendChild(card);
}
});
</script>
</body>
</html>
We create an empty .cards
list, which we’ll populate dynamically. For that, we use the HTML5 fetch
function. It returns promises, which are a compositional pattern for callbacks.
This is just about the most technical part of all of this. I promise it’ll get easier, but if you want to learn more about fetch
, check out https://developer.mozilla.org/en/docs/Web/API/Fetch_API. It works in recent versions of Chrome and Firefox, so use those to ensure these examples work!
We can work this client into our PHP server script:
$action = "/index";
$actions = ["/index", "/get"];
// ...
if ($action == "/index") {
header("Content-type: text/html");
print file_get_contents("index.html") and exit;
}
Now we can load the client page, and get a list of cards, all from the same script. We don’t even need to change how we run the PHP development server – this should just work!
A case for web sockets
This is all pretty standard PHP/JavaScript stuff. Well, we are using a new JavaScript feature, but it’s pretty much the same as $.ajax
.
If we wanted to add realtime functionality to this, there are a few tricks we could try. Say we wanted to add realtime card removal; we could use Ajax requests to remove cards, and Ajax polling to refresh other clients automatically.
This would work, but it would also add significant traffic to our HTTP server (even for duplicate responses).
Alternatively, we could open persistent connections between browsers and the server. We could then push new data from the server, and avoid polling continuously. These persistent connections can be web sockets, but there’s a problem…
PHP was created, and is mostly used, to serve requests quickly. Most scripts and applications aren’t designed to be long-running processes. Because of this, conventional PHP scripts and applications are really inefficient at holding many connections open at once.
This limitation often pushes developers towards other platforms (like NodeJS), or towards new architectures (like those provided by Icicle, ReactPHP, and AMPHP). This is one problem Socketize aims to solve.
Socketize to the rescue!
For brevity, we’re going to use the un-authenticated version of Socketize. This means we’ll be able to read and write data without authenticating each user. Socketize supports authentication, and that’s the recommended (and even preferred) method.
Create a Socketize account, by going to https://socketize.com/register:
Then, go to https://socketize.com/dashboard/member/payload/generate, and generate a key for admin
. Note this down. Go to https://socketize.com/dashboard/account/application for your account key (“Public Key”). Now we need to add some new JavaScript to our HTML page:
<script src="https://socketize.com/v1/socketize.min.js"></script>
<script type="application/javascript">
var params = {
"public_key": "[your_public_key]",
"auth_params": {
"username": "admin",
"payload": "[your_admin_key]"
}
};
var socketize = new Socketize.client(params);
socketize.on("login", function(user) {
var user = socketize.getUser();
console.log(user);
});
</script>
If you replace [your_public_key]
and [your_admin_key]
with the appropriate keys, and refresh the page, you should see a console message: “[Socketize] Connection established!”. You should also see an object describing the Socketize user account which is logged in. Note the user id
.
What does this mean? Our HTML page is now connected to the Socketize database (for our account). We can read and write from named lists of messages. Let’s change our server script to write the initial list of cards to a named Socketize list. All Socketize API requests take the form of:
curl "https://socketize.com/api/[method]?[parameters]"
-u [your_public_key]:[your_private_key]
-H "Accept: application/vnd.socketize.v1+json"
We can create a request
function, to simplify this process:
function request($method, $endpoint, $parameters = "")
{
$public = "[your_public_key]";
$private = "[your_private_key]";
$auth = base64_encode("{$public}:{$private}");
$context = stream_context_create([
"http" => [
"method" => $method,
"header" => [
"Authorization: Basic {$auth}",
"Accept: application/vnd.socketize.v1+json",
]
]
]);
$url = "https://socketize.com/api/{$endpoint}?{$parameters}";
$response = file_get_contents($url, false, $context);
return json_decode($response, true);
}
This method abstracts the common code for using the endpoints we’ve set up. We can see it in action, with the following requests:
$json = json_encode([
"id" => 1,
"name" => "Mysterious Challenger",
]);
request(
"PUT",
"push_on_list",
"key=[your_user_id]:cards&value={$json}"
);
request(
"GET",
"get_list_items",
"key=[your_user_id]:cards"
);
It’s probably best to use a robust HTTP request library, like GuzzlePHP, to make requests to third-party services. There’s also an official PHP SDK at: https://github.com/socketize/rest-php, but I prefer just to use these concise methods against the JSON API.
The request
function takes an HTTP request method, Socketize API endpoint and query-string parameter. You’ll need to replace [your_public_key]
, [your_private_key]
, and [your_user_id]
with the correct values. The two example calls should then work for you. cards
is the name of the list to which we write this card object. You can think of Socketize like an HTTP version of an object store.
We can adjust our HTML page to pull items from this list. While we’re at it, we can add a button to remove unwanted cards:
var socketize = new Socketize.client(params);
var cards = document.querySelector(".cards");
cards.addEventListener("click", function(e){
if (e.target.matches(".card .remove")) {
e.stopPropagation();
e.preventDefault();
socketize.publish("removed", e.target.dataset._id);
socketize.updateListItem("cards", null, e.target.dataset._id);
}
});
socketize.subscribe("removed", function(_id) {
var items = document.querySelectorAll(".card");
for (var i = 0; i < items.length; i++) {
if (items[i].dataset._id == _id) {
items[i].remove();
}
}
});
socketize.on("login", function(user) {
var user = socketize.getUser();
socketize.getListItems("cards")
.then(function(json) {
for (var i = 0; i < json.length; i++) {
var name = document.createElement("span");
name.className = "name";
name.innerHTML = json[i].name;
var remove = document.createElement("a");
remove.dataset._id = json[i]._id;
remove.innerHTML = "remove";
remove.className = "remove";
remove.href = "#";
var card = document.createElement("li");
card.dataset._id = json[i]._id;
card.className = "card";
card.appendChild(name);
card.appendChild(remove);
cards.appendChild(card);
}
});
});
The match
method works in recent versions of Chrome and Firefox. It’s not the only way to do it, but it is much cleaner than the alternative.
In this example, we add a click event listener, intercepting clicks on .remove
elements. This is called event delegation, and it’s super efficient! This way we don’t have to worry about removing event listeners on each .remove
element.
Each time a .remove
element is clicked, we trigger an update to the Socketize list. At the same time, we publish a removal
event. We also listen for removal
events, so that we can update our list if other users remove cards.
We can update the dataset
properties of elements. These are the JavaScript representation of data-*
HTML attributes. This way, we can store the Socketize _id
of each card. Finally, we adjust the code to create new elements, and append the generated elements to the .cards
element.
Where do we go from here?
This tiny example should illustrate how to begin doing useful stuff with Socketize. We added web socket communication (reading and writing to named lists, and publishing ad-hoc types of events). What’s awesome is that we did this without significantly altering our server-side code. We didn’t have to re-write for an event-loop. We just added a function or two, and we could push events to the browser.
Think of the interesting things you could do with this. Every time you save a database record, you can push an event to the browser. You can connect gamers to each other. You can alert conference speakers when they are close to attendees with questions. All of this in a traditional PHP codebase.
If you want to get better at this stuff, perhaps consider making an interface for adding and reordering cards. If you have questions, or can think of other interesting uses for this kind of persistent communication; leave a comment below.
Frequently Asked Questions about WebSockets in Your Synchronous Site
What is the difference between synchronous and asynchronous WebSockets?
Synchronous WebSockets wait for a response from the server before moving on to the next task, while asynchronous WebSockets can send and receive data simultaneously without waiting for a response. This makes asynchronous WebSockets more efficient for real-time applications, as they can handle multiple requests at once without blocking the main thread.
How do I implement WebSockets in PHP?
To implement WebSockets in PHP, you can use libraries such as Ratchet or Textalk. These libraries provide a simple API for creating WebSocket servers and clients. You can also use the built-in WebSocket API in PHP, but it requires a more in-depth understanding of the WebSocket protocol.
Why are WebSockets not supported in web workers?
Web workers are designed to perform tasks in the background without blocking the main thread. However, they do not have access to the DOM or other web APIs, including WebSockets. This is because web workers run in a separate thread and do not have access to the same resources as the main thread.
How can I build a real-time application with PHP WebSocket clients?
To build a real-time application with PHP WebSocket clients, you need to create a WebSocket server that can handle multiple connections and send and receive data in real-time. You can use libraries such as Ratchet or Textalk to simplify this process. Once your server is set up, you can use JavaScript on the client side to connect to the server and send and receive data.
What is the WebSocket protocol?
The WebSocket protocol is a communication protocol that provides full-duplex communication between a client and a server over a single, long-lived connection. It is designed to be implemented in web browsers and web servers, but it can be used by any client or server application.
How do I use the WebSocket API in PHP?
To use the WebSocket API in PHP, you need to create a new instance of the WebSocketClient class and use the connect method to connect to a WebSocket server. You can then use the send and receive methods to send and receive data.
Can I use WebSockets with other programming languages?
Yes, WebSockets can be used with many different programming languages, including JavaScript, Python, Ruby, and Java. Each language has its own libraries and frameworks for working with WebSockets.
How do I handle WebSocket connections in a synchronous site?
In a synchronous site, you can handle WebSocket connections by creating a new WebSocket server and listening for connection requests. When a request is received, you can accept the connection and start sending and receiving data.
What are the benefits of using WebSockets?
WebSockets provide real-time, bidirectional communication between a client and a server. This makes them ideal for applications that require real-time updates, such as chat applications, multiplayer games, and live data feeds.
Are there any limitations or drawbacks to using WebSockets?
While WebSockets offer many benefits, they also have some limitations. For example, they require a persistent connection between the client and the server, which can consume more resources than traditional HTTP requests. Additionally, not all web browsers support WebSockets, so you may need to provide a fallback for older browsers.
Christopher is a writer and coder, working at Over. He usually works on application architecture, though sometimes you'll find him building compilers or robots.