A Roadmap for PHP Learning

A detailed look at what is the next step to getting your skills to a professional standard

Matt Burgess
18 min readJan 22, 2017

One of the really great things about PHP is it’s got a super-low barrier to entry, but a nice path towards gradual improvement as you get more experience and knowledge.

Unfortunately, for a new developer it can be hard to know what that path is. If you lack the knowledge, terminology, and overall goal then it’s difficult to begin that journey.

I’m not going to say for a second that you should always be building enterprise OOP systems in advanced frameworks… you can start really basic and then enhance.

So that is what I’m going to do. Start with some entry level code, and step through it a bit at a time. For the most part this is actually more or less my own career path. I hope to prevent people getting stuck in the same places I did.

This article is not intended to as a comprehensive tutorial. The scope of some sections (such as OOP) would simply be too large, they could be be articles of similar length all on their own. Rather it is intended to provide a guide, a roadmap for self-education. A significant amount of information and code will be left out. In particular, this will be code that is moved out of our main file, such as that setting up database connections or auto loading patterns.

It’s also important not to feel like all of these things should be rushed through. This learning below probably covered around 10 years of my career. Don’t feel obliged to understand all of it in one go. This is merely intended to cover the “what should I be learning next” question.

It’s also worth noting that some of the code in here may be wrong. Again, it’s not intended as a tutorial, but a guide towards self-learning.

Our starting point

This would be a common starter approach. It’s the lowest common denominator of PHP code, and the format and syntax used by hundreds of extremely terrible tutorials that Google is only too happy to provide.

<?php
include_once 'database.php’;
include_once 'header.php’;
$sort = mysql_real_escape_string($_GET['sort']);
$result = mysql_query('SELECT * FROM cupcakes ORDER BY ' . $sort . ' ASC');
echo '<h1>My Cupcakes</h1>';
echo '<table id="cupcake-table">';
echo '<tr>';
echo '<th><a href="?sort=flavour">Flavour</a></th>';
echo '<th>Description</tr>';
echo '<th><a href="?sort=price">Price</a>';
echo '</tr>’;
while($row = mysql_fetch_assoc($result)){
echo '<tr>';
echo '<td class="col_flavour">' + $row['flavour'] + '</td>';
echo '<td class="col_desc">' + $row['description'] + '</td>';
echo '<td class="col_price">' + $row['price'] + '</td>';
echo '</tr>';
}
echo '</table>’;
include_once 'footer.php';
?>

Code has been written like that since the beginning of time. It will work. It definitely will work.

If you look, though, you’ll see pretty much all of its crap is all tangled together. How you get stuff from the database and how you loop over it, and how you display a table are all tied in. The PHP builds a big nasty chain of echoes, and reading all that concatenation is kind of a drag. The PHP makes it hard to read the HTML, and vice versa.

In fact, if you view source on this trying to debug the HTML that’s generated you’ll find it’s making it all on one line, and it will be a nightmare to work with. Adding new line characters will make your HTML better, but your PHP worse.

Breaking things apart

What we want to do is follow something called Separation of Concerns, which means breaking down your code into logical chunks. Technically it’s more about how objects are structured and interact, but the basic principle applies here.

Why are we doing this?

The more things are tangled up and depend on each other, the harder they are to work with. If you want to change your HTML, it’s a bit hard without your SQL stuff getting in the way. If you’re trying to fix something in the way the data is structured, you’ve got HTML in up in your grill. Bear in mind this is a simple example. An actual app would be much more complex and that gets exponentially more tangly.

What are we doing?

What we can do, though, just for now, is to split out our “getting data” and our “displaying data”. We don’t even need to make it a separate file, just a trivial change. I’m going to fix the mysql_ nonsense at the same time.

The mysql_ functions are deprecated, which means even if they work now they won’t do so forever. It would be tempting to replace them with identical functions called with mysqli_, which are not deprecated. But this only solves half the problem. The other half is that our code itself shouldn’t know or care what database is being used. MySQL, Postgres, SQLite, they’re all valid options. Caring about what exactly is being used shouldn’t be something the code exposes. So we’re going to use PDO, an abstraction library.

An abstraction is a layer over the top of something (in this case the database calls) that hides complexity. Computer science is built on good abstractions. Or you can set your own magnetised zeros and ones.

<?php
// set up document
include_once 'database.php';
include_once 'header.php';
// get data
$query = "SELECT * FROM cupcakes ORDER BY :sort ASC";
$statement = $database->prepare($query);$parameters = ['sort' => $_GET['sort']];
$statement->execute($parameters);
$cupcakesArray = $statement->fetchAll(PDO::FETCH_ASSOC);
?>
<!-- deal with html in html-land -->
<h1>My Cupcakes</h1>
<table class="cupcake-table">
<?php foreach($cupcakesArray as $cupcake) { ?>
<tr>
<td class="col-flavour"><?php echo $row['flavour'] ?></td>
<td class="col-desc"><?php echo $row['description'] ?></td>
<td class="col-price"><?php echo $row['price'] ?></td>
</tr>
<?php } ?>
</table>
<?php include_once 'footer.php' ?>

If you look at the above you can see the HTML is much cleaner and easier to work with. It breaks more cleanly into HTML, with bits of PHP as variables throughout. At this point you’ll usually find any decent editor or IDE, such as PHPStorm or Sublime Text will be able to tell this is HTML, and give you proper syntax highlighting, etc.

The code now also uses PDO, which means it’s not tied to any specific database, and we have access to prepared statements with named parameters. This prevents the risks mentioned earlier, the prepared statements correctly deal with all escaping, making much more readable (and secure) code.

What to learn

It’s worth knowing more about the functionality of PDO, in particular the prepared statement used above.

PDO
Prepared Statements

Making and Using Functions

You can clean this up even more by taking the data collection and moving it into a function.

Why are we doing this?

The main reason is that the code, when it’s a function, can be used by multiple different files without duplicating any code. You can simply call it with a different argument and get back whatever data you need.

What are we doing?

Though this function would typically be in another file, we’re going to just leave it at the top of this one. Leaving you with this:

<?php
include_once 'database.php';
include_once 'header.php';
function getCupcakes($database){
$query = 'SELECT * FROM cupcakes ORDER BY :sort ASC';
$statement = $database->prepare();
$parameters = ['sort' => $_GET['sort']];
$statement->execute($parameters);
return $statement->fetchAll(PDO::FETCH_ASSOC);
}
?>
<h1>My Cupcakes</h1><table class="cupcake-table">
<?php foreach(getCupcakes() as $cupcake) { ?>
<tr>
<td class="col-flavour"><?php echo $row['flavour'] ?></td>
<td class="col-desc"><?php echo $row['description'] ?></td>
<td class="col-price"><?php echo $row['price'] ?></td>
</tr>
<?php } ?>
</table>
<?php include_once ‘footer.php' ?>

Something to note here is that we have to pass in $database to our function. This is because of scopes. A scope is sort of the context you’re working in. The base level is “global” scope. All standard PHP functions exist in the global scope, and so does your new function. But inside your new function is a whole other environment, a clean slate. Variables defined in the global scope are not accessible unless they’re passed in, or you use the global $variable syntax to pull them in. And don’t do that.

You’ll also notice that our syntax has changed quite a lot from mysql_query to $database->prepare. This change from what is called procedural to object syntax is important, but we don’t need to focus on it too much now.

What to learn

User defined functions, scopes, why you shouldn’t use global.

Using globals in functions
Creating Functions in PHP
Variable Scopes

Using Templates

The next step is towards using templates in our PHP code. A lot of people will snarkily tell you that you don’t need to use templates, because PHP is already a templating language. This is partially true. While PHP is very good for displaying variables in HTML (as we were doing before) a dedicated system for doing that allows us to have a workable pattern and approach. We want to split of this display code into separate files that can be worked on independently. Additionally, using something that is not PHP allows us to have cleaner and more readable code, though we do have a learn a tiny bit of alternative syntax.

Why are we doing this?

We want to separate code out that doesn’t deal with the same thing. The template, for example is all HTML layout. It has nothing to do with databases. If you want to fix a markup bug or add a block of text you really don’t need SQL statements in the way.

We also want to clean up the global scope while making the functional code more re-usable.

What do we do?

We’re already very close to something you might consider a template, so let’s imagine we broke that chunk of code out and used Twig as a template object.

Now we have a file called cupcake_prices.php, and we can do the following.

<?php
// set up document
include_once 'database.php';
include_once 'functions.php';
include_once 'classes/twig.class.php';
$cupcakes = ['cupcakes' => getCupcakes($database)];
$twig->render('cupcake.html', $cupcakes);

That’s literally the whole thing. You don’t even need the ?php> bit because you’re not mixing in HTML. Your cupcakes data is handled in the and the twig class handles all your header and footer stuff. Typically you’d have more logic, like sorting or user input or whatever here, but this is all pretty static.

We haven’t changed our actual cupcake function, but we’ve moved it into a separate file just to make it easier to on ourselves. There’s no need to show that, it’s identical to the above.

Your cupcake.html, the template, file will also be cleaner than our previous pure PHP implementation.

{% extends "base.html" %}
{% block content %}
<h1>My Cupcakes</h1>
<table class="cupcake-table">
{% for cupcake in cupcakes %}
<tr>
<td class="col-flavour">{{ cupcake.flavour }}</td>
<td class="col-desc">{{ cupcake.description }}</td>
<td class="col-price">{{ cupcake.price }}</td>
</tr>
<?php } ?>
</table>
{% endblock %}

What you should learn

Twig syntax

Using Classes

We have been using a functions.php file to split out data access. However, that file could get enormous. What we want to do is make separate files for each type of thing, each “entity”. We could make something like cupcake_functions.php, but a better approach is a class. To illustrate the benefits we’ve also added a second function, that just gets a single cupcake by ID. This is obviously a pretty common requirement, and is definitely going to be needed by any non-trivial example usage.

<?php
Class Cupcake {
public function find($database, $id){
$query = 'SELECT * FROM cupcakes WHERE id = :id';
$statement = $database->prepare($query);
return $statement->fetch(PDO::FETCH_ASSOC);
}
public function all($database){
$query = 'SELECT * FROM cupcakes ORDER BY :sort ASC';
$statement = $database->prepare();
$parameters = ['sort' => $_GET['sort']];

return $statement->fetchAll(PDO::FETCH_ASSOC);
}
}

For a bit of quick explanation, we made a simple class.

The code shown in the section above will be more or less correct, except the way you access the data will be slightly different. Classes can be accessed by creating an instance of them first, then accessing it.

$cupcake = new Cupcake();
$data = $cupcake->all($database);

You can see from this that we get a quite nicely readable approach. Putting the functions in a class means we can give generic names like all while still being completely clear what it does.

What you should learn

Classes in PHP

Object Oriented Programming

I wasn’t really sure how far to go down this path. It’s way out of scope of this article to get deep into Object Oriented Programming, but the above really isn’t great. For all the functionality is in a class, it’s not really a useful object.

An object is supposed to essentially be a smart little thing, that has all the knowledge that should be required to do its job. Ours requires quite a bit of hand-holding.

We’re going to make a little change and make the code quite a bit more useful. To do this, we’re going to make use of its constructor. The constructor is a bit of code that executes when you use the new keyword to create an object. The syntax used is a bit odd. PHP likes to start its “magic” functions with a double underscore.

<?php
class Cupcake(){
private $database;
public __construct($database, $id = false){
$this->database = $database;
}

public function find($id){
$query = 'SELECT * FROM cupcakes WHERE id = :id';
$statement = $this->database->prepare($query);
return $statement->fetch(PDO::FETCH_ASSOC);
}
public function all(){
$query = 'SELECT * FROM cupcakes ORDER BY :sort ASC';
$statement = $database->prepare();
$parameters = ['sort' => $_GET['sort']];

return $statement->fetchAll(PDO::FETCH_ASSOC);
}
}

The real benefit here is that you don’t need to pass in $database to every function. This might seem trivial but it’s actually quite fundamental. This object now knows everything it needs in order to run. We pass it in one time at the start, and the system knows everything it needs — these are called dependencies. In fact, we could pass in different databases at will, and it will still work! We might do testing with a pre-set SQLite database, for example.

Usage is roughly the same as before, except for where we pass in the database as we create a new class.

$cupcake = new Cupcake($database);$allCupcakes = $cupcake->all();$oneCupcake = $cupcake->find(12);

We’d put our file in classes/cupcake.class.php in this case, just to keep it consistent.

Something else has been introduced at the same time. You’ll have noticed previously we have been writing public in a lot of places, and now we have a bunch of public and a single private.

This is called visibility. The public keyword says that the function can be called from outside the class, like we’ve done above. The private keyword says we can’t. $cupcake->database isn’t available to us. It’s only available to the functions calling from within the class. You’ll notice the $this->database calls inside the functions. $this in PHP refers to the class.

This actually brings us back to the discussion of scopes made previously. Within the context of running a class as an object, there’s a whole new scope. It’s probably the most important and useful scope in PHP, so it’s worth reading more about some of the gotchas here.

What to learn

Visibility

Autoloading

One of the biggest issues with the above is the bit of include 'stuff.php'; happening in the top. Again it’s worth clarifying that what we’re talking about here most affects complex systems. There might be dozens of files being included here, and those might be being included into dozens more. It can quickly become a maintenance problem.

You can have includes that include other includes, and then include those…. But this just adds to the problem, rather than solving it. Thankfully, PHP has an elegant solution – autoloading.

Autoloading tells PHP where to find the class files it’s looking for.

spl_autoload_register(function ($class) {
include 'classes/' . $class . '.class.php';
});

This would be a simple example. As long as we have classes/twig.class.php and classes/cupcakes.class.php we will always be able to use these without ever bothering to do our long list of includes.

We can make all of this easier on ourselves by using Composer.

Using Composer

Composer is a PHP package manager. If you look at something like Twig, for example, Composer will let us install and use Twig with minimal effort. It will also let us manage anything Twig needs installed (called dependencies) and will make sure it’s kept up-to-date.

Installing Composer depends on your operating system, but once it’s installed, packages can be installed just using the command line.

composer require twig/twig:~1.0

Critically, Composer builds an autoload file. By including this you get access to all of the packages Composer has put into the vendors directory without any need for us to think about, manage, or update. If you add new packages and functionality, no changes are required ever. They just all get handled by that autoload file.

This seems like a lot of overhead, and it absolutely is for our two or three files. But a real system may have dozens or even hundreds of interconnected requirements.

Important things you need to look into

Composer

Even more OOP

We’ve made our cupcakes an object, but it’s a fairly simple object. Objects of this kind work a lot better if the structure of the object more closely matches the data.

What are we going to do?

To illustrate the point, we’re going to add a save() function to our object. We’re also going to add a bunch of public properties to our model so that $cupcake->price works. We’re also going to namespace our class. Finally, we’re going to make it so that the constructor optionally takes an id.

Why are we doing this?

Having an object that represents the data closely makes it a lot easier to access and modify the data. In particularly, it’s a lot easier to keep a sort of mental model in your head of what things do.

Namespacing is used to keep clear what part of the application the file belongs to. With something like “Cupcake” it’s pretty clear, but a class called Users could belong to anything or have any role. And in particular, you could have multiple that might conflict. Adding a namespace allows PHP’s compiler to tell the difference between Authentication\Users and Profiles\MessageSettings\Users. Ours is going to be pretty generic.

<?php
use App;
class Cupcake(){
private $database;
public $id;
public $flavour;
public $description;
public $price;
public __construct($database, $id = false){
$this->database = $database;
if($id) {
$data = $this->find($id);
$this->id = $id;
$this->flavour = $data['flavour'];
$this-description = $data['description'];
$this->price = $data['price'];
}
}

public function find($id){
// unchanged
}
public function all(){
// unchanged
}
public function save(){
$query = "UPDATE cupcakes SET flavour = :flavour,
description = :description
price = :price WHERE id = :id";
$statement = $this->database->prepare($query);
$data = [
'id' => $this->id,
'flavour' => $this->flavour,
'description' => $this->description,
'price' => $this->price,
];
return $statement->execute($data);
}
}

Note that there are clever ways of doing the above $data array, including get_object_vars($this) or running json_encode then json_decode on $this, but this is a bit more clear and explicit.

What this does is allow really easy usage, especially when editing, for example in a simple admin system.

$cupcake = new Cupcake($database, 12);
$cupcake->price = '4.95';
$cupcake->save();

Our actual example usage is unchanged for the most part, but actually slightly simplified. Especially in the case where we’re using it to find a single item. In fact, let’s create a new page, this one is just cupcake.php and is the page for a single cupcake.

$cupcake = ;
// set up document
include_once 'database.php';
include_once 'functions.php';
include_once 'classes/twig.class.php';
$cupcake = new Cupcake($database, $_GET['cupcake_id']);$twig->render('cupcake.html', $cupcake);

We’ve actually stumbled on a design pattern here. This is called “Active Record”. The active record pattern just treats each row of the database as an object. It’s a simple way to think about and understand data, and is ideal for systems that have simple input and output of data — CRUD: create, read, update, delete.

There are a lot of things we could do to make this smarter and more powerful, but we have diminishing returns here, and this is a huge topic.

What to learn

Active record pattern — Note that while this page does a good job of describing the pattern it also uses mysql_query.

Implementing MVC

MVC means Model View Controller. It suggests that you split your code into three separate layers. The Model layer is often referred to as the data, but more accurately represents “business logic”, the data and rules that define your application. The View is anything that is presented to the user. Most commonly this will be HTML, but returning something like JSON is increasingly common, and is still in the MVC pattern entirely happily.

The Controller layer is the intermediary. It is the only part of the system that actually needs to understand what was requested, and how it should be responded to. The URL, any query parameters, any post data.

The controller takes the request information and uses it to generate a response. Most commonly it takes the request, uses it to figure out what data is needs, asks the Model layer for the data, puts that data into the relevant View layer, and returns that rendered out HTML.

In our example, our Cupcake class represents our Model layer. The View layer is handled by Twig. The controller, then, is the actual page itself.

What’s the benefit

This pattern is an intuitive one, and one that many people (including me) discover for themselves long before ever seeing it given a name. It’s a clear separation of concerns. We let each area of the site deal with its own stuff. This means that while building or maintaining the system we’re not going to be needing to cross from the controller to the model much, and certainly not from the model to the view.

What we need to do

The next step is a bit less intuitive. We need to give up on our pricelist.php. We need to move to a front controller, often more commonly known as a router. We’re going to change things up a lot here. Actually making a router is nonsensical, so we’re just going to install one.

Using a Microframework

Installing a microframework will give us a better understanding of the intent and structure of a framework, but still allow us to use all the code we’ve got written already.

We can install the Silex framework with composer.

composer require silex/silex "~2.0"

We are now going to make or update an index.php file and make it essentially the same as our previous cupcake_prices.php.

<?php
require_once 'autoload.php';
$app = new Silex\Application();$app->get('cupcake_prices', function() {
$cupcake = new Cupcake($database);
$data = ['cupcakes'=>$cupcake->all()];
return $app['twig']->render('cupcake_prices.html', $data);
});
$app->get('cupcake/{id}', function($id) {
$data = new Cupcake($database, $id);
return $app['twig']->render('cupcake_single.html', $data);
});
$app->run();

The benefit here is that we can see so much of our code working so clearly. We’ve even set up our single cupcake listing, though we haven’t bothered to demo the template for that.

Again, if you imagine half a dozen different routes, like a homepage, contact form, contact form submission, order form… you can see that this sort of structure will stay entirely comprehensible. After a time it would be necessary to split them up into separate controllers, but regardless, this is a nice clean start.

Using an ORM

Our data so far has been simple, limited to a single table. But data will almost never actually be this simple. Data will be accessed in a number of different ways, with a whole bunch of different “where” conditions. More particularly, tables will need to be joined, whether lookup tables for something like status_id or category_id, or whole other entities. A common example, orders will have both cupcakes and customers.

We actually want to avoid having a lot of SQL. It can end up being a lot of work to maintain. Adding a field to a table means modifications all through the system. Different contexts (the orders for a customer, the customers for a day, the cupcakes for an order, the most popular cupcakes) require subtly different queries, and they multiply out of control.

We could modify our Cupcake class to add a bunch of functionality to add “where” conditions to our “all” or add individual functions to get the data in different contexts, but a better approach (as always) is to let someone else do it, and use an existing ORM.

An ORM is an Object Relational Mapper. This is essentially a single place where you can define the relationships between your entities. An order has many cupcakes. An order belongs to a customer. An order has one status.

As long as these are defined in the ORM the data can be sliced and diced however it’s required.

We’re going to implement the Eloquent ORM used by the Laravel framework. It’s a nice system to use, and it follows the Active Record pattern we stumbled on earlier.

With Eloquent you define your models, much like we defined our Cupcake class earlier. In fact, our Eloquent model replaces the Cupcake class. The Eloquent base stuff handles the vast majority of what we need to do, and all we need really is any relationships.

<?php
namespace App;
Use Illuminate\Database\Eloquent\ModelClass Cupcake extends Model {
protected $fillable = ['flavour', 'description', 'price']
protected $timestamps = false;
}

Eloquent has a bunch defaults set up. The model assumes a table of the plural version of its own name. I.e., cupcakes. It assumes an id that’s an auto-incrementing integer. Both of these assumptions are already valid for our table, making it an easy setup. It also assumes two fields for datetime last updated and datetime created. This isn’t true for us, so we’ve put false for that.

Actually using these is super easy. It’s near identical to our previous code, and not by accident.

The only difference is

$cupcake = new Cupcake($database);
$data = ['cupcakes'=>$cupcake->all()];

Is replaced with this

$data = ['cupcakes' => Cupcake::all()];

The benefit here is tenuous. Again we hit the issue that this application is too simple to benefit from the changes we’re making. And again, it’s vital to understand that basically any real application will be doing a ton of stuff. It will have a specials page, admin system, possibly a search, order system, your customers might have an order summary, and so on. All of the above is much easier to do with something like an ORM. (These assume data exists that isn’t actually present in our examples.)

//customer’s total orders
App\Order::where('customer_id', $customer_id)->sum('total')->get();
// cupcakes in an order
App\Order::find($order_id)->cupcakes;
// cupcakes that are on special
App\Order::where('on_special', true)->order('price')->get();

None of this requires any modifications or maintenance. The addition of new fields to the table doesn’t mean any changes to update or creation SQL, and there aren’t multi-line SQL statements scattered through your classes.

Doing it in Laravel

The next and final step is an exercise for the reader. We’re at the level we need to be at now where the only real approach is to use a full framework. The benefits here are many. We get a lot of great features, like the ORM, database migrations, testing frameworks, and more. Now that you’re here, if you want a great guide to using Laravel you won’t find a better resource than Laracasts.

--

--

Matt Burgess

Senior Web Developer based in Bangkok, Thailand. Javascript, Web and Blockchain Developer.