using Programming;

A Blog about some of the intrinsics related to programming and how one can get the best out of various languages.

Getting Started with PHP: Part 4

Getting started with PHP: Part 4

Last time we went over more of the PHP function details. Particularly, we covered the following:

  • Default values in functions;
  • Functions can be late-defined;
  • Functions can have variable-length arguments;
  • Accessing global variables in functions / scoping;
  • Standard lib: array <-> string;
  • Standard lib: trimming strings;
  • Standard lib: searching strings;
  • Standard lib: manipulating strings;
  • Standard lib: hashing strings;

This set us up to know more about how PHP functions work, what kinds of functions we can expect, and advanced features of function definitions. This time, we're going to go into something called "object-oriented programming", and talk about a PHP feature known as "classes".

What are classes

A "class" in PHP is a collection of values and functions that typically perform an individual behaviour. We mentioned them a while back in lesson 2 when I talked about HTML form controls, and that I have a PHP library of my own which defines a plethora of these functions.

Obviously one of the issues with putting everything in the PHP global scope is that you run into trouble when you have two functions but they would have a similar name. In these cases, you end up with things like:

function user_is_valid(int $user_id): bool
function post_is_valid(int $post_id): bool
function comment_is_valid(int $comment_id): bool
function email_is_valid(string $email): bool

Classes allow us to put some logical barriers between them. They allow us to name all these functions is_valid, but put them in different locations to ensure the names don't conflict with each other.

Very often, we'll use classes to encapsulate groupings of functions and pieces of data. We might make a class for a User component, a class for a Database component, a class for Html helpers, for Parsing data, a class of String functions, etc.

The first component of classes we'll talk about are "instance" functions. These functions require you to have created a "version" or "copy" or "instance" of the class.

PHP: Classes

Before we can talk about an "instance function" in a class, we actually have to talk about how to define a class. In PHP, classes are defined rather simply: the class keyword, followed by a name, then curly-braces ({}) to define what exists within the class:

class HtmlControls {
}

At this point, if we use a sufficiently-advanced development tool, when we type Html it will give us HtmlControls as an option, though we currently have no logic in it.

There are some additional things you might see after the name and the opening brace ({): extends $name, where $name is the name of another class, and implements $name, where $name is the name of an interface. Ignore these for now, we'll talk about them later.

In PHP, we try to ensure that a class has a "single" responsibility and name it appropriately. A "single" responsibility just means that the class has one "job" or "goal". The idea is that the class is supposed to do "a" thing, is all.

PHP: Classes - Instance Functions

So now that we know what a class is there are two ways we define things in the class: either as a "static" or "always accessible" member, or an "instance" member. We'll start with instance members, because they allow us to dive into classes at a more reasonable pace.

We define members in a class much in the same way we would in the global scope. To start with, we define functions with the function keyword, then the exact same semantics as global functions. Thus, to put our checkbox function in our class, we just define it like normal:

class HtmlControls 
{
    function checkbox(string $name): ?string {
        if (isset($_POST[$name]) && $_POST[$name] !== "off") {
            return $_POST[$name];
        }
        return NULL;
    }
}

We defined this function as a standard "instance" function, and by default PHP treats these as "public" functions, but we'll talk about that in a little bit.

To call this function, we have to create an instance of the class and then call the function. To create an instance, we define a variable to hold the instance, then use new $class_name to create it:

$controls = new HtmlControls();

At this point, we can call any instance functions with $controls->$function_name. So, to call our checkbox function, we simply do $controls->checkbox($checkbox_name).

PHP: Classes - Static Functions

We typically use instance functions to define behavior that should be tied to data within the class. However, in our case, our behavior isn't tied to other data within the class, so an instance function doesn't necessarily make sense. In this case, we can use a static function.

To define a static function we just put the word static before the word function in our definition:

class HtmlControls
{
    static function checkbox(string $name): ?string {
        if (isset($_POST[$name]) && $_POST[$name] !== "off") {
            return $_POST[$name];
        }
        return NULL;
    }
}

There is a difference in how we call these functions. While we have to new HtmlControls and then ->checkbox($checkbox_name) for an instance function, static functions are a bit easier to call. For a static function, we simply $class_name::$function_name for static functions. In our example, that would be HtmlControls::checkbox($checkbox_name).

In our case, a static function makes more sense because the behaviour we are defining doesn't depend on any other items within our class.

The next thing to discuss is class data. We discussed the is_user_valid function before, when we talked about global scope, but now we'll put it into the context of a class.

PHP: Classes - Values / Variables

In order to have functions work with class data, we have to talk about class data. To define variables in a class, we define them with a visibility modifier (private, protected, public) then a variable name.

class User
{
    public int $id;
    public string $name;
}

But, I don't want to just throw those names at you, so we're going to talk about visibility modifiers really quick.

PHP: Visibility Modifiers

PHP has three visibility modifiers:

  • private: the item is only usable by any other items within the same class;
  • protected: the item is only usable by any other items within the same class or any derivative class;
  • public: the item is usable by items within and outside the class;

Many times, to properly encapsulate object-oriented data, we will actually define class variables as private, and then define public functions to get and set the data. The reason for this is that if we have to validate the data when it gets set, we don't want to change the behavior of all the callers unless we need to.

PHP: Back to Classes - Values / Variables

With visibility modifiers out of the way, we can talk about how we might access them outside the class. Much like accessing functions, we use $variable->$name:

$user = new User();
$user->name = "Elliott";
echo $user->name;

Just like any variable, using $this->$name = $value will set it, and omitting the = $value will "get" it.

As mentioned, we want to encapsulate these variables in a different manner, specifically to prevent the need to change behavior everywhere we use them if we ever add validation logic. To do that, we'll discuss accessing instance data from another instance member.

PHP: Classes - Accessing Instance Data

We know how to define instance variables, we need to discuss how we access them. Unlike standard variables, they aren't accessed by using $name. We actually have to access them with a special "instance" object, called $this.

This means we can change our $id and $name variables to private, and define a getId and getName function that will return these values:

class User
{
    private int $id;
    private string $name;

    public function getId(): int {
        return $this->id;
    }
    public function getName(): string {
        return $this->name;
    }
}

So now, we can get these values, but we also need to be able to set them:

public function setId(int $id) {
    $this->id = $id;
}
public function setName(string $name) {
    $this->name = $name;
}

Once that's done, we would set and get our name with a minor change to our code:

$user = new User();
$user->setName("Elliott");
echo $user->getName();

All that out of the way, we can put our isValid function back in:

public function isValid(): bool {
    return $this->id !== 0 && $this->name !== "";
}

But, there's one more major component of classes I want to talk about: constructors.

PHP: Classes - Constructors

The next point about classes to discuss here is constructors. Constructors allow us to specify that the new $class_name() call requires parameters, and means we can remove our need to $user->setName() by including the $name in the constructor.

In PHP we accomplish constructors via a magic function called __construct.

We define this function in our class like any regular function, but the special name __construct causes it to be seen by any new $class_name() calls. This means we can force callers to provide certain things when they create an instance of the class, ensuring that certain behaviour is guaranteed from the beginning.

For our User class, it might look something like this:

public function __construct(int $id, string $name) {
    $this->id = $id;
    $this->name = $name;
}

Now, when we try to instance a new User, we will need to use that constructor. It forces us to provide the $id and $name on creation of a User.

$user = new User(1, "Elliott");

We can still use the default values on parameters if we like, which means we can create a constructor that requires some, or even none of the values to be provided. This is because __construct is a regular function, just with a special name.

PHP: Classes - Constants

Lastly, I want to cover constants in classes. Constants can be used to provide data that doesn't change to callers. They're defined with the const keyword as well as whichever access modifier is appropriate. Additionally, the dollar-sign is not used for constant definitions.

In PHP, I typically like to write my constant names as UPPER_SNAKE_CASE. As an example, we might keep a constant for the table name that we store user data in, that way anywhere we use it we make sure it's always the same. This means if we ever change the user table name, we don't need to change it everywhere we use it, we only change it in the one location.

To define our constant, we just put it in our class:

private const USER_TABLE = "Users";

The purpose of constants is to create a single source of truth so that you cannot use the wrong value, or forget the value you were using.

PHP: Including Other Code

At this point, it's time to discuss how we include other code within our PHP application. Putting all of our code in one big file is not always the best way to build our application. A very common and good practice in software engineering is to separate individual components of an application into reasonable chunks. Yes, we could put all of our code in a single, enormous file. But that makes it more difficult to troubleshoot and modify in the future.

So, with PHP this is actually a well-thought-out process. There is a group of 4 functions that each play a role.

With PHP, we have the ability to write something in one PHP file, and then include that something in another PHP file. The method to do so depends on what exactly is being accomplished, but there are two pairs of functions we use: include, include_once, and require, require_once.

The include and include_once functions will pull another PHP file into the calling file. The include_once function will guarantee that the file does not appear more than once. This can be important if you include a file in multiple places throughout your application. In some cases, you may wish to ensure that it is only included a single time, because it might have class definitions or function definitions that cannot appear more than once.

The require and require_once functions will do the same thing, with the exception that they will throw a fatal error if the file cannot be included for some reason. If your application depends on something to the point where it cannot work without it, you likely want require and require_once. I find that, most commonly, I use require_once, and if something can appear more than once I use require.

The reason that the _once version exist is because you can't redefine things, as we've mentioned briefly before. As an example, imagine you have the following:

class MyClass
{
    function myFunction(): string {
        return "Hello World";
    }
}
class MyClass
{
    function myFunction(): string {
        return "Hello World";
    }
}

PHP will error on the second class MyClass line, because you redefined this. With require and include that stands true as well. This makes it critical to understand how to protect your PHP code from this issue.

As an example, imagine we have the following structure:

Database.php

<?php
require_once("User.php");

class Database
{
    function getUser(int $id): User {
        return new User($id);
    }
}

User.php

<?php
class User
{
    private int $id;

    public function __construct(int $id) {
        $this->id = $id; 
    }

    public function getId() {
        return $this->id;
    }
}

ViewUser.php

<?php
require_once("User.php");
require_once("Database.php");

if (isset($_GET["id"])) {
    $database = new Database();
    echo $database->getUser($_GET["id"])->getId();
}

With this example, we can build the following tree of includes:

ViewUser.php
|-- User.php
|-- Database.php
    |-- User.php

Because we used require_once instead of require, the User.php file will not actually be included by the Database.php file, but instead by the ViewUser.php file. Additionally, we won't get any errors about duplicate definitions.

However, if we changed that to require, the application would fail on require_once("Database.php") because it would attempt to include the User.php file again, and would duplicate the class User definition.

PHP: Interfaces

The next major object-oriented discussion point is interfaces.

Interfaces are a way of providing a common specification for implementations to fulfill. The idea is that you can design a specification that you need, and then allow implementors to switch out how they fulfill it.

As an example, we may have a situation where we have a "data provider". The "data provider" may provide functions to read and write various objects, but it might provide them for different storage platforms. I.e. we may have a MySqlDataProvider, a PostgreSqlDataProvider, an XmlFileDataProvider, and so on.

To do this, we might have an interface that defines what our data provider must do.

Much like a class, an interface is defined using the interface keyword, then the name of the interface. I like to prefix my interface names with an I, it comes from a naming convention for a different language, but it also allows you to look at the name and understand that you're not actually dealing with a hard-class.

However, unlike a class, you cannot create an instance of an interface. You cannot new $interface_name(), because an interface does not have any functionality. It is strictly a list of "requirements". In some languages it's also called a "protocol".

Once you've defined the interface via name, you identify the functions and variables that are part of the interface requirements. This is done by defining the function like normal, but not providing an implementation via curly-braces ({}) but simply ending with a semi-colon (;).

For a data provider, we might define the following interface:

interface IDataProvider
{
    function loadUser(int $id): User;
    function saveUser(User $user);
}

At this point, we have created the protocol or specification, and we can now implement it. To do so, we'll define a class that implements the interface via the implements keyword. This is added after the class name and before the opening curly-brace ({), and afterwards we include the names of the interfaces we want to implement.

class XmlDataProvider implements IDataProvider
{
    function loadUser(int $id): User {
        return new User($id, "Elliott");
    }
    function saveUser(User $user) {

    }
}

Obviously we didn't load or save the User, but we do have the structure for doing so. Now we can instance the XmlDataProvider:

$data_provider = new XmlDataProvider();

Now, at this point, it might not be clear why we used the interface at all: we only have one data-provider, and we create it directly, the only place we see IDataProvider is the implements keyword.

The purpose of the interface is to allow us to create functions and other classes that use the interface definition, so that we can provide any class that implements the interface.

function saveUserFromForm(IDataProvider $data_provider) {
    $id = $_POST["id"];
    $name = $_POST["name"];
    $user = new User($id, $name);
    $data_provider->saveUser($user);
}

Now, we can give the function any IDataProvider, such as our XmlDataProvider:

saveUserFromForm($data_provider);

This means that if we later built a MySqlDataProvider, we can literally swap what gets passed to the saveUserFromForm function, and we would automatically be using our new provider.

The bigger concept here is "abstraction": that is we move common or higher-level functionality into another layer of our application. Something like the data provider details don't matter to the users of those components. The users only care that the component works and that they can use it.


As It Stands

At this point we actually have enough information to start building real things. We'll go over new topics as we encounter them, and as we build stuff we'll learn a lot. We have a lot more information to cover: from more standard-library functions and classes to more syntax bits we have to learn yet. That said, we're going to start building real-world stuff next, and hopefully that demonstrates how powerful PHP is.