PHP
Article

Starting a New PHP Package The Right Way

By Bruno Skvorc

How To Build Your Own PHP Package

Back when we covered Diffbot, the visual AI-enhanced machine-learning crawler, we also mentioned they have libraries for a wide array of programming languages, but those are often less than cutting edge – with so many to keep an eye on, there’s bound to be a few bad apples that slip through the cracks. One such apple is their PHP library, and we’ll be building an alternative in this series, in an effort to do it better.

Note that this tutorial will focus on writing a good package – the code we write will be real and production-ready, but you shouldn’t focus too much on Diffbot itself. Diffbot’s API is simple enough and Guzzle’s interface is smooth enough to just consume it outright without the need for a PHP library anyway. Rather, pay attention to the approaches we use to develop a high quality PHP package, so you can reuse them in your own project. Diffbot was selected as the subject of the package because I’d like to demonstrate best practices on a real world example, rather than yet another “Acme” package.

Good Package Design

In recent years, good standards for PHP package design have popped up, in no small part due to Composer, Packagist, The League and, most recently, The Checklist. Putting all these in a practical list we can follow here, but avoiding any tight coupling with The League (since our package won’t be submitted there – it’s specifically made for a third party API provider and as such very limited in context), the rules we’ll follow are:

  1. include a license
  2. be open source (well, duh!)
  3. exclude development stuff from dist
  4. use PSR-4 autoloading
  5. be hosted on Packagist for Composer installation
  6. be framework agnostic
  7. use PSR-2 coding standard
  8. have in depth code comments
  9. use semantic versioning
  10. use CI and Unit Tests

For a more detailed reading of these and more rules, see here.

Getting Started

Naturally, we’ll be using our trusty Homestead Improved box again, as it’s the quickest way to get started developing on a unified environment. For your reference, I chose the following vhosts, and will be using them throughout the rest of this tutorial:

sites:
    - map: test.app
      to: /home/vagrant/Code/diffbot_lib
    - map: test2.app
      to: /home/vagrant/Code/diffbot_test

OK, after getting into the VM, let’s start hacking away.

To hit the ground running, we’ll use the League Skeleton, which is a template package with League rules embedded, allowing for a head start. I’ve made my own fork with a better .gitignore and some minor tweaks if you’d like to use that – if not, just use theirs, the difference is truly trivial.

git clone https://github.com/Swader/php_package_skeleton diffbot_lib

We edit the composer.json file and end up with something like this:

{
    "name": "swader/diffbot_client",
    "description": "A PHP wrapper for using Diffbot's API",
    "keywords": [
        "diffbot", "api", "wrapper", "client"
    ],
    "homepage": "https://github.com/swader/diffbot_client",
    "license": "MIT",
    "authors": [
        {
            "name": "Bruno Skvorc",
            "email": "bruno@skvorc.me",
            "homepage": "http://bitfalls.com",
            "role": "Developer"
        }
    ],
    "require": {
        "php" : ">=5.5.0"
    },
    "require-dev": {
        "phpunit/phpunit" : "4.*"
    },
    "autoload": {
        "psr-4": {
            "Swader\\Diffbot\\": "src"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Swader\\Diffbot\\Test\\": "tests"
        }
    },
    "extra": {
        "branch-alias": {
            "dev-master": "1.0-dev"
        }
    }
}

We set some common meta data, define the requirements, and set up PSR-4 autoloading. This, along with the fact that we’re using the League Skeleton, takes care of points 1-6 on our to-do list from above. While we’re here, we can also add Guzzle to our requirements, as it’s the HTTP client library we’ll be using to make all calls to Diffbot API points.

"require": {
        "php" : ">=5.5.0",
        "guzzlehttp/guzzle": "~5.0"
    },

After running composer install, which will pull in all the dependencies, including PHPUnit we’ll need for testing, we can check if everything is working by changing the contents of src/SkeletonClass.php to:

<?php

namespace Swader\Diffbot;

class SkeletonClass
{

    /**
     * Create a new Skeleton Instance
     */
    public function __construct()
    {
    }

    /**
     * Friendly welcome
     *
     * @param string $phrase Phrase to return
     *
     * @return string Returns the phrase passed in
     */
    public function echoPhrase($phrase)
    {
        return $phrase;
    }
}

and creating an index.php file in the root of the project:

<?php

require_once "vendor/autoload.php";

$class = new \Swader\Diffbot\SkeletonClass();

echo $class->echoPhrase("It's working");

Visiting test.app:8000 in the browser should now yield the “It’s working” message.

Don’t worry about not having a public directory or anything of the sort – this is not important when building a package. When building a library, all the focus should be on the package, and only on the package – no need to deal with frameworks or MVC. We’ll use the index.php file for testing some stuff out from time to time, but mostly, we’ll be using PHPUnit to develop our library. For now, let’s add index.php to .gitignore to make sure we don’t accidentally send it upstream.

PSR-2

To keep in sync with modern standards, we’d do best to implement PSR-2 from the get-go. I use PhpStorm, so this is dead easy to do. You can either choose the built-in PSR1/PSR2 standards, like so, or you can install and activate CodeSniffer and use it as a PhpStorm inspection, like so. I opted for the former, because remote execution of PHPCS via PhpStorm is not yet supported (and a Vagrant VM is, for all intents and purposes, remote), but if you’d like to help out with adding this feature to PhpStorm, please vote here.

You can still require CodeSniffer in your project as usual via Composer and run it from the VM’s command line, though:

You can also opt to only install PHP on your host machine (as opposed to additional nonsense that comes with a standard XAMPP/WAMP installation), download CodeSniffer there, and use it like that. You’d use your host machine only for code inspection, while developing and running your package logic from the VM. It’s a bit awkward, but helps when using IDEs like PhpStorm, at least until the aforementioned issue is implemented.

If you’re not using PhpStorm, look for alternatives on how to accomplish this, but make sure you do – we need PSR2.

Planning

With our bootstrapping out of the way, we can start developing. Let’s think about everything we need.

Entry point

No matter what the use case for Diffbot’s API, a user will want to create an instance of the API client – there’s nothing you can do with Diffbot other than query the pre-made APIs. Each API use also needs a developer token which is to be passed in the request as a query param, in the form of ?token=xxxxxx. My reasoning is as follows: a single developer will usually be using a single token, so aside from allowing developers to create new API client instances and passing in a token (say, in the constructor), we should also have a way of defining a global token to be used in all future instantiations. In other words, we want both of these approaches to be valid:

$token = xxxx;

// approach 1
$api1 = new Diffbot($token);
$api2 = new Diffbot($token);

// approach 2
Diffbot::setToken($token);
$api1 = new Diffbot();
$api2 = new Diffbot();

The former approach helps when you’re creating a single API client instance, or you’re using several tokens (maybe you have one for Crawlbot, and one for regular APIs). The latter approach works well when you’ve defined many API endpoints for your application to consume and will be needing several, but don’t want to re-inject the token every time.

With that in mind, let’s go ahead and make our package’s first class. Create the file src/Diffbot.php.

<?php

namespace Swader\Diffbot;

use Swader\Diffbot\Exceptions\DiffbotException;

/**
 * Class Diffbot
 *
 * The main class for API consumption
 *
 * @package Swader\Diffbot
 */
class Diffbot
{
    /** @var string The API access token */
    private static $token = null;

    /** @var string The instance token, settable once per new instance */
    private $instanceToken;

    /**
     * @param string|null $token The API access token, as obtained on diffbot.com/dev
     * @throws DiffbotException When no token is provided
     */
    public function __construct($token = null)
    {
        if ($token === null) {
            if (self::$token === null) {
                $msg = 'No token provided, and none is globally set. ';
                $msg .= 'Use Diffbot::setToken, or instantiate the Diffbot class with a $token parameter.';
                throw new DiffbotException($msg);
            }
        } else {
            self::validateToken($token);
            $this->instanceToken = $token;
        }
    }

    /**
     * Sets the token for all future new instances
     * @param $token string The API access token, as obtained on diffbot.com/dev
     * @return void
     */
    public static function setToken($token)
    {
        self::validateToken($token);
        self::$token = $token;
    }

    private static function validateToken($token)
    {
        if (!is_string($token)) {
            throw new \InvalidArgumentException('Token is not a string.');
        }
        if (strlen($token) < 4) {
            throw new \InvalidArgumentException('Token "' . $token . '" is too short, and thus invalid.');
        }
        return true;
    }
}

The method also references a DiffbotException, so real quick, just make the file src/exceptions/DiffbotException.php with the contents:

<?php

namespace Swader\Diffbot\Exceptions;

/**
 * Class DiffbotException
 * @package Swader\Diffbot\Exceptions
 */
class DiffbotException extends \Exception
{

}

Let’s quickly explain the Diffbot class.

The token static property will serve as the default which Diffbot will use if no token is provided in the constructor while building a new instance. In that case, it gets copied into the instanceToken property which is bound to instances.

The constructor checks if a token was passed. If it wasn’t, it uses the predefined default token, or throws a DiffbotException if none was set – that’s what our Exception code above was for. If the token is OK, it gets set as the token of the instance. On the other hand, if the token was passed in, then that one gets copied into instanceToken. Note that in both cases, the token must be validated with the validateToken static method. This private method for now simply checks if the token is a string of a length of more than three characters – if not, it throws an invalid argument exception.

Finally, there’s the setToken static method, which lets us set the aforementioned global token. Naturally, this one needs to get validated, too.

Seeing as a Diffbot token is bound to its set of APIs, being able to change a token on an already existing instance of Diffbot would be crazy. As such, I’ve opted to allow the setting of a token only on instantiation, or globally for all future instances of Diffbot. Of course, if the token gets set globally, an instance can still override this setting. Also, the global token is mutable, because we want to be able to change the spawning condition of future instances, and changing it when instances already exist doesn’t affect them in any way.

Notice also how everything is documented with docblocks – not overdocumented, but just enough to make it easy to understand for everyone else coming in.

Conclusion

In this part, we got started with PHP package development by setting up a skeleton project with some basic functionality, and by configuring our environment. You can download the end result of part 1 here. In part 2, we’ll start writing some tests and some actual functionality, and we’ll get started with proper test driven development. Before we move on, are there any questions or comments regarding the current process? Leave them below!

Comments
gekkie

Just a quick response to your improved gitignore fork: its a common rule to leave your IDE,OS specific artifacts in your local global ignore file so you don't clutter the ignore file with your specific environment...

Looking forward to the rest of the series!

swader

Yes, I've talked about this with League, and they were actually surprised at my Git setup. I'm a bit particular in regards to that in that I don't have a global Git - I use Git from within the VM exclusively. This makes sure my entire setup is in the VM and the VM alone, making it 100% portable to any and all machines I come in contact with. I know most people don't work that way, but it works wonders for me. Thanks for the feedback!

gekkie

Thats another thing i've been wondering about: why work inside the VM like that? With tech like docker (which I personally use extensively, even inside PHPstorm) i have the very best of performance and still have a clean development machine.

(i.e. no PHP install on my dev, but I've mapped my PHP executable in PHPstorm to a docker run command. This too for CodeSniffer which is in comparison with locally installed PHPCS negligibly slower without the hassle of version conflicts per project etc Currently we run Grunt, NPM, PHPCS, Bower, PHP, PHPUnit all from docker +1 )

But back on topic: why not add those to a global gitignore in the VM then? I'd rather see you illustrate the best way and explain why then to go against such a commonly accepted method ( https://github.com/github/gitignore / http://augustl.com/blog/2009/global_gitignores/ )

swader

Docker is great, when you're not working with Windows. I, however, am.

Windows is my host machine, and keeping Git and related tools up to date on it is a pain. Keeping my configuration inside the VM keeps all my development efforts 100% separate from the host, meaning I can not only rely on the VM to have the most up to date tools I need at any time on any machine, but also that I can just grab the vagrant folder and take it somewhere with me.

If I were able to run docker on Windows without a middle-step like boot2docker or similar, then yes, things would probably look different. But when I take into consideration the various OS differences, I come to the conclusion that the average effort to get started is lowest when using this approach of mine. That's not to say this isn't to change as the technologies evolve, but currently, this is the setup I've found works instantly and trouble-free for the greatest number of people. I've yet to see any complaints about it.

You are right, however, that these should be added to the global gitignore inside the VM. I will make an effort to do so in the next release of Homestead Improved, thanks for the suggestion.

codepunker

Hi @swader,

I had a look at the cool things The PHP League has done. I especially looked at their League\Csv package and it is indeed very nicely coded - really inspiring.

However, I noticed that it took more than 6 months of commits to take it where it is now. I honestly feel that it is unrealistic to think that any real life project will allow for that much time to develop one module, one tiny part of a project.

My point is that it will be very hard to implement such strict standards inside commercial PHP products when dealing with economics more than with standards.

What do you guys think ? Maybe I just haven't (yet) met the right clients ?

TaylorRen

Run phpcs with SF2, a lot of ERRORs from the auto-generated stuffs (Entity/*, for example).

swader

Well, you shouldn't wait for League packages - just pick those you like and use them, no? Or did I miss your meaning? Standards are very easy to implement when done from scratch - just set your IDE to use them and you'll never stray. Don't allow other devs not to use them, and your team is automatically in sync.

s_molinari

I have to agree about using a VM on Windows. Someone much smarter than me also pointed me to PuPHPet, Vagrant and Virtualbox and wow, what a difference. I will never go back to using Windows for the server environment again.

Scott

swader

I can't even imagine myself developing on bare Windows any more, no matter how many packages exist to make things simpler. Vagrant has drastically changed the way I develop.

s_molinari

Yes, all one needs is a halfway decent computer with a bit of extra RAM and away we go! smiley

Scott

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.