Starting PHP Development in 2019: Project Setup and Dependencies

I was experimenting with PHP development over the weekend to whip up a properly unit-tested library to generate license codes, hosted on your own web server. I wanted to make use of my newfound emacs prowess and figure out how programming is supposed to work with the stuff I have and have not prepared so far.

But how do you start a PHP project? From the Ruby community, I know that there are widely-adopted conventions regarding the root-level directory structure that some tools even rely on. Convention is part of Ruby on Rails's magic, so I'm eager to figure out what the PHP community adopted so far to get productive quickly.

Modern PHP is object-oriented and comes with Java-like namespaces. The namespace of the app or library here is replaced with LIBRARYNAME. Insert your own package name there. There probably is a tool to set all of this up for you, but I didn't stumple upon one and did all the following manually, figuring out how namespace resolving etc. works.

Directory structure

The basic structure I adopted is:

  • src/LIBRARYNAME to put all my module code into;
  • tests/LIBRARYNAME contains all the tests, mirroring the directory structure of src.
  • config/ for the development and production environment changes. I want to use SQLite locally and MySQL/Maria DB on the server.
  • public/ contains web-facing files, i.e. the index.php that will create a server instance and forward requests. That'll end up in the public htdocs of my server.
  • vendor/ contains dependencies, managed by the PHP Composer dependency manager.

Project settings

PHPUnit allows configuration via XML files. It's optional, but I figured I might as well add the default configuration to a file. So phpunit.xml reads:

<?xml version="1.0" encoding="utf-8" ?>
<phpunit bootstrap="./vendor/autoload.php">
    <testsuites>
        <testsuite name="The project's test suite">
            <directory>./tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

Then this is the project's composer.json I have:

{
  "license": "MIT",
  "authors": [
    {
      "name": "Christian Tietze",
      "email": "hi@christiantietze.de",
      "homepage": "https://christiantietze.de"
    }
  ],
  "autoload": {
    "psr-0": {
      "LIBRARYNAME": "src/"
    }
  },
  "scripts": {
    "test": [
      "phpunit -c phpunit.xml"
    ],
    "serve": [
      "php -S localhost:8888 -t public/"
    ]
  },
  "require": {
    "php": "^7.1"
  },
  "require-dev": {
    "phpunit/phpunit": "^8.4"
  }
}

I require PHP 7.1 or newer to get optional return values in type annotations. (See "Misc" below.)

I added the latest PHPUnit library, and also created two script shorthands. I can run compose test and have phpunit execute in the project context with my project settings. I can also spin up the PHP server for local testing from the public/ subdirectory without cd'ing inside. Nice.

The autoload convention I use here is "psr-0". I didn't get "psr-4" to work right away and figured I might as well use the version 0 convention. It basically expects the LIBRARYNAME namespace inside the src/ subdirectory. Namespaces are resolved as part of the directory structure, so it'll look in src/LIBRARYNAME/ for files of that namespace.

With the project metadata set up, now to actual code.

Production and Development Environment Configurations

I added a type that handles the configurations in src/LIBRARYNAME/Config.php:

<?php
namespace LIBRARYNAME;

/**
 * Configuration pattern via <https://stackoverflow.com/a/3724689/1460929>
 */
abstract class Config {
    /** Storage for DB / passwords etc */
    static private $protected = array();

    /** Storage for public, aka client-facing metadata */
    static private $public = array();

    public static function getProtected($key) {
        return isset(self::$protected[$key]) ? self::$protected[$key] : false;
    }

    public static function getPublic($key) {
        return isset(self::$public[$key]) ? self::$public[$key] : false;
    }

    public static function setProtected($key, $value) {
        self::$protected[$key] = $value;
    }

    public static function setPublic($key, $value) {
        self::$public[$key] = $value;
    }

    public function __get($key) {
        return isset(self::$public[$key]) ? self::$public[$key] : false;
    }

    public function __isset($key) {
        return isset(self::$public[$key]);
    }
}

With this configuration type, which is just a glorified dictionary (or rather: PHP array), here's config/config.dev.php:

<?php
use LIBRARYNAME\Config;

Config::setProtected("environment", "this is the development environment");

The actual setting is nonsensical, but it serves the purpose of demonstrating how to use the type. In config/config.prod.php I set a different value.

Please note that the first line of code is not require __DIR__."/../vendor/autoload.php" which a lot of scripts on the web are using. I don't know why. Maybe the autoloader was less powerful when the projects started; maybe I am excluding part of the world's PHP configuration by not requiring the autoload.php script here. This works for me so far, because public/index.php is requiring the autoloader already, and the configuration files are not individually executed.

Serving different configuration from the web

When I run composer serve, my public/index.php is listening at port 8888. This file currently takes care of switching configuration files:

<?php
require __DIR__."/../vendor/autoload.php";
ini_set('display_errors', 1);
error_reporting(E_ALL);

define("DEVELOPMENT", 0);
define("PRODUCTION", 1);
define("DEFAULT_ENV", DEVELOPMENT);
define("ENV",
       (getenv("ENV") == "production")
           ? PRODUCTION
           : ((getenv("ENV") == "development")
               ? DEVELOPMENT
               : DEFAULT_ENV));

function loadConfiguration() {
    $suffix = (ENV == PRODUCTION) ? "prod" : "dev";
    $filename = "config.".$suffix.".php";
    require_once __DIR__."/../config/".$filename;
}

loadConfiguration();

print "Current env: ".LIBRARYNAME\Config::getProtected("environment")."\n";

This is not very interesting; it defines a couple of environment constants first, then sets the current ENV depending on the actual environment setting and a fallback value. Then the matching config file is loaded from disk.

This script only prints the nonsensical configuration variable so far.

I can switch environments by setting an environment varaible when running the program:

$ ENV=production composer serve
$ ENV=development composer serve

That's all I need to run the server with each setting. I like that part, though I didn't use it so far, heh.

Writing tests

Here's a single unit test for a value type in the domain I'm working on. I picked the most interesting one.

The test file is tests/LIBRARYNAME/HashablePayloadTest.php:

<?php
use PHPUnit\Framework\TestCase;
use LIBRARYNAME\HashablePayload;

final class HashablePayloadTest extends TestCase {
    public function testHash_TwoDataParameters_ReturnsMD5OfValuesSortedsByKeyAndPrivateKeySuffix() {
        $privateKey = "the private key";
        $data = array('unsorted' => '123', 'data' => '456');
        $payload = new HashablePayload($data, $privateKey);

        $result = $payload->hash();

        $this->assertEquals(
            md5('456123'.$privateKey),
            $result
        );
    }
}

This is, by the way, the algorithm used by FastSpring for order completion callbacks. A shared key is appended to the concatenation of all request values (sorted by key), then an MD5 checksum is generated and passed along with the request. Since the key is private, this is already pretty hard to break.

And here's the implementation, src/LIBRARYNAME/HashablePayload.php:

<?php
namespace FastSpringMate;

function array_ksorted(array $data) : array {
    $result = $data;
    ksort($result);
    return $result;
}

final class HashablePayload {
    public function __construct(array $data, string $privateKey) {
        $this->data = $data;
        $this->privateKey = $privateKey;
    }

    public function data() : array {
        return $this->data;
    }

    public function hash() : string {
        return md5($this->sortedDataString().$this->privateKey);
    }

    private function sortedDataString() : string {
        private function sortedDataString() : string {
        return join(array_values(array_ksorted($this->data)));
    }
}

Miscellanea

  • Autoloading seems to be widely adopted, albeit in different ways; composer comes with its own implementation that seems to Just Work, so I stick to that
  • PHP supports type annotations for parameters and return values! function foo(string $input = "default") : int
  • PHP 7.1 supports optional return values, too, so you can return null when object creation fails, for example: function bar() : ?string
  • Unlike Swift, PHP doesn't have failable initializers. You are supposed to throw exceptions when the initialization fails.
  • When you use namespaces and autoloading, during tests every type will be assumed to be part of project namespace, including RuntimeException. To tell PHP that you want the type from the global PHP namespace and not want to look for a type of this name inside your project namespace, prefix it with a \\RuntimeException.
  • Namespaces in general still look weird: LIBRARYNAME\Namespace1\Namespace2\TypeName. Like DOS paths.
  • PHP closures have explicit capture lists in use(...). For example: function ($param) use ($captureThis) { return $captureThis == $param; }
  • Composer is PHP's dependency and package manager. Use it to create a project and set up dependencies. If you know npm or rubygems, you know composer.
  • PHPUnit Skeleton shows a simple directory structure for a library that's separated into lib/APPNAME and tests/APPNAME with PHPUnit and Composer settings
  • PHP Settings type to store public and protected settings.
  • PHP The Right Way is a collection of coding standards; for a beginner like me, this is as good as any convention, so I immediately liked it!
  • PHP Best Practices similarly showed me that you can rely on autoloading nowadays. That's nice, and with a composer-based package, it also doesn't require any work on your part.
  • A nice example project setup: OAuth2 Demo PHP is a package that you run as a server, which in turn depends on the OAuth2 Server PHP library. The library is unit tested and does not have a web-facing directory; the server in turn has mostly settings and virtually no logic.

Browse the blog archive