The Workshop: Producing Packages (Part 1) - php[architect] Magazine October 2018

Joe • September 1, 2020

learning packages phparch writing

Warning:

This post content may not be current, please double check the official documentation as needed.

This post may also be in an unedited form with grammatical or spelling mistakes, purchase the October 2018 issue from http://phparch.com for the professionally edited version.

The Workshop: Producing Packages (Part 1)

by Joe Ferguson

This month we are diving into creating PHP packages and ensuring our packages are held to a high standard of having tests, license information, and other best practices for creating and maintaining high quality PHP packages.

Composer changed the entire PHP ecosystem by giving us Composer, a fantastic package manager and Packagist, the public repository of libraries. PHP developers can now easily share their code for others to use. You can also easily bundle your own applications domain logic into a package to share between multiple projects. Private Packagist is a commercial offering to allow developers to host their proprietary code in a secure place away from the public.

Step 1: Build a library

Math is hard, it's one of my least favorite subjects and I became a programmer because I hated math and learned enough to program the computer to do the math for me. So to make math even easier on our fellow PHP developers as well as an easy to follow example package.

We are intentionally keeping the work of our example library simple as what the library does is not our point. Our point is to demonstrate what makes a package "good" and how we can improve package quality. You can find our package on github.com.

Start with the basics

We're going to build a simple math library which will provide users of our library with addition and subtraction functions to use. We'll start with a basic composer.json which has some information about our package and declare our dependencies of PHP 7.2 and a development dependency of PHPUnit. We're going to require PHP 7.2 because at the time of creating this library 7.2 is the most recent version. Also 7.2 gives us new features such as parameter type widening if we want to use an interface we'll be able to omit any type restrictions and using emulated prepare functions in PDO to help Debug what SQL is being sent to the database server.

{
    "name": "svpernova09/php-easy-math",
    "description": "A simple library to do math for you",
    "require": {
        "php": "^7.2"
    },
    "require-dev": {
        "phpunit/phpunit": "^7.3"
    },
    "autoload": {
        "psr-4": {"EasyMath\\": "src/"}
    }
}

We are also adding an autoload section to specify we want to use PSR-4 autoloading to map our src folder to the namespace \EasyMath.

The src folder

The src/ folder is shorthand for "source" as in source code which we're going to put our library's logic.

Starting with an Addition class which has one method named add()taking two parameters and adds them together.

<?php
namespace EasyMath;

class Addition
{
    public function add($x, $y)
    {
        return $x + $y;
    }
}

Now we have addition covered lets add a subtraction class with a subtract method with takes two parameters and subtracts the second value from the first:

<?php
namespace EasyMath;

class Subtraction
{
    public function subtract($x, $y)
    {
        return $x - $y;
    }
}

An example use of our library in any PHP project would look something like this example.php:

<?php

use EasyMath\Addition;
use EasyMath\Subtraction;

require __DIR__ . '/vendor/autoload.php';

$add = new Addition();
echo $add->add(3, 2) . PHP_EOL;

$sub = new Subtraction();
echo $sub->subtract(3, 2) . PHP_EOL;

When we execute this code we'll see 5 and 1 output to the command line:

$ php example.php                                                                           
5
1

Great, our application is telling us 3 plus 2 is 5 and 3 minus 2 is 1. How can we ensure our code is going to be consistent? How can we test multiple scenarios to ensure the math is correct? We'll write tests!

The tests folder

The tests folder is where our unit tests will live. We also will create a phpunit.xml file in the root of our project to set up and configure several PHPUnit behaviors so we can easily just execute the phpunit binary found in the vendor/bin folder. The PHPUnit XMl configuration file we're going to use will be:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Unit">
            <directory>./tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

Since our library is quite simple our tests are equally as simple but we will add a data provider so we can easily test multiple scenarios without having to write a lot of repetitive code.

We'll start by creating an AdditionTest class:

<?php
namespace EasyMath\Tests;

use EasyMath\Addition;
use PHPUnit\Framework\TestCase;

final class AdditionTest extends TestCase
{
    /**
     * @param $x
     * @param $y
     * @param $expected
     * @dataProvider mathProvider
     */
    public function testEasyMathKnowsHowToAdd($x, $y, $expected)
    {
        $math = new Addition();

        $this->assertEquals(
            $expected,
            $math->add($x, $y)
        );
    }

    public function mathProvider()
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 2],
            [1337, 1337, 2674],
        ];
    }
}

If you are new to testing data providers might look confusing at first glance. We have our test method testEasyMathKnowsHowToAdd which takes three paramters $x, $y, and $expected and also contains an annotation named @dataProvider. The annotation tells PHPUnit to look for a method named mathProvider() and use the output of the method as input for our test splitting each sub-array of the array as each parameter. Looking at the first array which contains zero, zero, zero, PHPUnit will translate to testEasyMathKnowsHowToAdd(0, 0, 0) so our test will execute theadd()method with the first two zeros as$xand$yand the third zero as$expectedso we can useassertEquals()to ensure$expectedequals$math->add($x, $y)which translates to: $this->assertEquals(0, 0 + 0)`.

We can run our AdditionTest and see the output:

$ ./vendor/bin/phpunit tests/AdditionTest.php
PHPUnit 7.3.5 by Sebastian Bergmann and contributors.

.....                                                               5 / 5 (100%)

Time: 59 ms, Memory: 4.00MB

OK (5 tests, 5 assertions)

Great! Our addition test cases are all passing. Now we can build tests/SubtractionTest.php:

<?php
namespace EasyMath\Tests;

use EasyMath\Subtraction;
use PHPUnit\Framework\TestCase;

final class SubtractionTest extends TestCase
{
    /**
     * @param $x
     * @param $y
     * @param $expected
     * @dataProvider mathProvider
     */
    public function testEasyMathKnowsHowToSubtract($x, $y, $expected)
    {
        $math = new Subtraction();

        $this->assertEquals(
            $expected,
            $math->subtract($x, $y)
        );
    }

    public function mathProvider()
    {
        return [
            [0, 0, 0],
            [0, 1, -1],
            [1, 0, 1],
            [1, 1, 0],
            [2674, 1337, 1337],
        ];
    }
}

Running both tests together gives us the following output:

$ ./vendor/bin/phpunit
PHPUnit 7.3.5 by Sebastian Bergmann and contributors.

..........                                                        10 / 10 (100%)

Time: 45 ms, Memory: 4.00MB

OK (10 tests, 10 assertions)

Continuous Integration

Now we have a passing test suite we need to wire up a continuous integration service to run our tests whenever we push code and especially when pull requests are opened on our repository. Travis-CI makes continuous integration incredibly easy and free for open source applications.

We want to click the "Sign in with Github" link in the top right of the travis-ci.org site and authenticate with our Github.com credentials. Once complete we'll be presented with a list of repositories we can enable Travis-CI (Travis). Because our package is quite simple we don't need to do any more set up here but if you need a database or extra configuration make sure you read through the PHP documentation.

Before we can trigger our first build we need to create a .travis.yml configuration file in the root of our project. This file will be parsed by Travis and execute based on our specifications. Our math library configuration will look like:

language: php
php:
- '7.2'
- nightly
matrix:
  allow_failures:
  - php: nightly
before_script: composer install
script: vendor/bin/phpunit

Our configuration will tell Travis our library is a PHP application and we want to test against PHP 7.2 and nightly which would be the latest nightly build of PHP, at the time of this writing would be the latest build available of PHP 7.3. We also specify a matrix setting so we can allow_failures for the nightly PHP version in case something there breaks our build will not show as broken. We're allowing these failures without failing the entire build because we want to look to the future to ensure our library is compatible with upcoming PHP changes. We also specify before the script runs we want to install our dependencies via Composer. Lastly our script command will execute the PHPUnit binary to run our tests.

We're not quite ready to execute our first Travis build just yet, but if you want to look ahead at what a build looks like take a peek at the version 1.0.0 build then come back and continue reading.

Sharing our package

Packagist is the central public repository of all public PHP packages. When we run composer commands we're interacting with Packagist. Before we sign into Packagist with our Github account we want to make sure we're logged out of Github.com so when we login to Packagist we're prompted to approve the permissions needed for Packagist on Github. Once completed we can sign into Packagist with our Github account and click on the submit link in the top right of the website. To submit a package we just need to enter the Github URL of our repository and click "check" to verify the repository name and then click "Submit" to finish the process. You can see what our math package looks like here: https://packagist.org/packages/svpernova09/php-easy-math.

It's very important we follow semantic versioning so users of our library will know when we have breaking changes based on major, minor, or patch values in the version. You can see we have an initial release 1.0.0 of our library. This means our library is ready for production use by users. Users can now consume our package in their applications by running composer install svpernova09/php-easy-math.

Semantic versioning is based on Major.Minor.Patch version numbers so that you can ensure upgrading from 1.0.1 to 1.0.2 should not introduce any backwards incompatible changes while upgrading from 1.0.2 to 1.3.0 will indicate there are some minor changes to be aware of.

Since we signed out of Github.com before we logged into Packagist our repository should be connected to Packagist so every time we publish a new release it will automatically be picked up on Packagist.org. If Packagist shows you an error message you can review how to update packages.

What's next?

Since we now have a usable package can you spot the really important open source thing we forgot to do? If you have a guess send me a message on Twitter. Check back next month as we add the important thing we forgot this time and we learn about some tooling to help ensure our packages are held to higher quality standards.

Warning:

This post content may not be current, please double check the official documentation as needed.

This post may also be in an unedited form with grammatical or spelling mistakes, purchase the October 2018 issue from http://phparch.com for the professionally edited version.