[go: up one dir, main page]
More Web Proxy on the site http://driver.im/

Starting a New PHP Package The Right Way

Share this article

Key Takeaways

  • Good standards for PHP package design have emerged in recent years, including the use of a license, open-source code, excluding development stuff from dist, using PSR-4 autoloading, hosting on Packagist for Composer installation, being framework agnostic, using PSR-2 coding standard, having in-depth code comments, using semantic versioning, and using CI and Unit Tests.
  • The League Skeleton can be used for a head start in PHP package development, with minor tweaks possible for individual preferences. The composer.json file can be edited to include common meta data, define requirements, and set up PSR-4 autoloading.
  • To keep in sync with modern standards, implementing PSR-2 from the beginning is recommended. This can be achieved through built-in PSR1/PSR2 standards or by installing and activating CodeSniffer.
  • The Diffbot class in PHP package development allows for the setting of a token only on instantiation, or globally for all future instances. The global token is mutable, allowing for changes to the spawning condition of future instances without affecting existing ones. The class also includes docblocks for easy understanding.

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!

Frequently Asked Questions (FAQs) about Starting a New PHP Package

What is the importance of a README file in a PHP package?

A README file is a crucial component of any PHP package. It provides essential information about the package, including its purpose, how to install it, and how to use it. This file is typically the first thing users see when they encounter your package, so it’s important to make it as clear and informative as possible. A well-written README can significantly improve the user experience and increase the adoption rate of your package.

How can I ensure my PHP package is compatible with different PHP versions?

To ensure your PHP package is compatible with different PHP versions, you can use tools like PHPUnit for testing. PHPUnit allows you to write tests for your code and run them across different PHP versions. This way, you can identify and fix any compatibility issues before they affect your users. Additionally, you should clearly state the PHP version requirements in your package’s documentation.

What is the role of a .gitignore file in a PHP package?

A .gitignore file is used to tell Git which files or directories to ignore in your project. This is particularly useful in a PHP package, where you might have files that are necessary for your local development environment but should not be included in the final package. Examples of such files include configuration files, log files, and dependencies installed by Composer.

How can I manage dependencies in my PHP package?

Managing dependencies in a PHP package is typically done using Composer, a tool for dependency management in PHP. With Composer, you can specify the libraries your project depends on, and it will manage (install/update) them for you. You can list your dependencies in a composer.json file, and Composer will handle the rest.

How can I distribute my PHP package?

One of the most common ways to distribute a PHP package is through Packagist, the default package repository for Composer. By submitting your package to Packagist, you make it easily accessible to other developers. They can then include your package in their projects by simply adding it as a dependency in their composer.json file.

How can I ensure the quality of my PHP package?

Ensuring the quality of your PHP package involves writing clean, efficient code, thoroughly testing your code, and maintaining good documentation. Tools like PHPUnit for testing, PHP_CodeSniffer for code quality checks, and phpDocumentor for generating documentation can be very helpful in this regard.

How can I handle versioning in my PHP package?

Versioning in a PHP package is typically handled using semantic versioning. This involves versioning your package in the format of MAJOR.MINOR.PATCH, where you increment the:

    1. MAJOR version when you make incompatible API changes,

    1. MINOR version when you add functionality in a backwards-compatible manner, and

    1. PATCH version when you make backwards-compatible bug fixes.

How can I contribute to other PHP packages?

Contributing to other PHP packages typically involves forking the package’s repository, making your changes, and then submitting a pull request. It’s important to follow the package’s contribution guidelines, which are usually found in a CONTRIBUTING file in the repository.

How can I handle errors and exceptions in my PHP package?

Handling errors and exceptions in a PHP package is typically done using try-catch blocks. This allows you to catch exceptions and handle them gracefully, rather than letting them cause a fatal error. It’s also a good practice to throw meaningful exceptions in your code when something goes wrong.

How can I keep my PHP package secure?

Keeping your PHP package secure involves following best practices for secure coding, regularly updating your dependencies to include security fixes, and using tools like SensioLabs Security Checker to check for known security vulnerabilities in your dependencies.

Bruno SkvorcBruno Skvorc
View Author

Bruno is a blockchain developer and technical educator at the Web3 Foundation, the foundation that's building the next generation of the free people's internet. He runs two newsletters you should subscribe to if you're interested in Web3.0: Dot Leap covers ecosystem and tech development of Web3, and NFT Review covers the evolution of the non-fungible token (digital collectibles) ecosystem inside this emerging new web. His current passion project is RMRK.app, the most advanced NFT system in the world, which allows NFTs to own other NFTs, NFTs to react to emotion, NFTs to be governed democratically, and NFTs to be multiple things at once.

Best PracticesBrunoSOOPHPpackagePHPphp5php7phpleague
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week