Create a library with composer

What is Composer?

Just as NPM is to JavaScript, Composer is the dedicated tool for PHP that allows you to manage library dependencies for your applications. If you've used a framework like Symfony, you've definitely used it. In reality, every PHP developer uses it on a daily basis. If you're on this page, you're probably wondering how to create a library that can be easily redistributed and shared with the community. That's what I'll try to show you.

For the sake of our tutorial, I'll take a concrete example of a fix that I had to apply to a project recently. I really asked myself, "Why isn't there already a tool to fix this easily?". So, I'm going to take this small fix and turn it into a Composer library and maybe, in addition to allowing me to write this article, this library will be useful to someone else.

Context: I inherited a project with a problem of deserialization of data inserted into a database. The developer had the good idea to use serialize on an array. Well, it's not what I would have done, but that's how the app is made and in itself it's not too bothersome. Except that apparently, the data went from one DB to another (mysql->mssql->mysql) generating encoding problems. We end up with a lovely unserialize error at offset. The error is quite verbose. PHP's serialize format, which indicates the type followed by the number of characters, allows us to quickly diagnose that the calculated number of characters is not right. It's just a matter of recalculating and rewriting it in the file with a regex and a callback.

For a reminder, here's the doc on the format https://en.wikipedia.org/wiki/PHP_serialization_format

image-1

So our error comes from the deserialization process when the number of characters specified for a string does not match what is decoded by the unserialize function. So you have to recalculate. For example:

s:6:"apple";

should be:

s:5:"apple";

It's quite simple, but perfect for our example!

Let's get started! We're going to get our hands dirty.

Prerequisites

We will need:
– PHP installed in CLI on our machine,
– an account on github,
– an account on packagist
– Composer installed on our machine.

Presumably, if you are here it's because you have already installed PHP and Composer. If not yet, you can find plenty of resources on the Internet to explain how.

So let's start by creating our project under our favorite editor, for me it's PhpStorm. At this stage, you should have an empty directory.

The following command will allow you to initialize your Composer project and enrich it with basic information such as the License, the name, the repository address, and a short description.

composer init -q -a src \
--name partitech/fix-serialize \
--description "Fix Php unserialize error at offset" \
--license MIT

You will go from an empty directory to an architecture designed for Composer. It's quite magical.

image-2
{
    "name": "partitech/fix-serialize",
    "description": "Fix Php unserialize error at offset",
    "license": "MIT",
    "autoload": {
        "psr-4": {
            "Partitech\\FixSerialize\\": "src"
        }
    },
    "require": {}
}

src: Contains your code
vendor: Contains all dependencies. Yes, your library might need other Composer libraries to work.
composer.json and composer.lock are files you are surely familiar with 😉

Like any good library, we will test our code at every stage of our development, and to do this we will install, guess what... PhpUnit.

composer require --dev phpunit/phpunit ^9.5

We will configure PhpUnit with the famous phpunit.xml and add the right references in composer.json.

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
    <testsuites>
        <testsuite name="common tests">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

Your composer file should broadly look like this.

{
    "name": "partitech/fix-unserialize",
    "description": "Fix Php unserialize error at offset",
    "license": "MIT",
    "autoload": {
        "psr-4": {
            "Partitech\\FixUnSerialize\\": "src"
        }
    },
    "require-dev": {
        "phpunit/phpunit": "^9.5"
    },
    "scripts": {
        "test": "phpunit"
    }
}

You can create the tests directory and run the command composer test. There are no tests at the moment, but this will validate that everything is set up correctly.

mkdir tests
composer test
image-3

It's finally time to create our first dummy test which will allow us to create the files we need.

<?php

namespace Partitech\FixUnSerialize\Tests;

use Partitech\FixUnSerialize\UnSerializeService;

class FixUnSerializeTest extends \PHPUnit\Framework\TestCase
{
    public function testSayHelloWorld()
    {
        $helloWorld = new UnSerializeService();
        $this->assertEquals("Hello World!", $helloWorld->unserialize('Hello'));
    }
}
<?php
namespace Partitech\FixUnSerialize;

class UnSerializeService
{
    /**
     * @param string $data
     * @return string|null
     */
    public function unserialize(string $data): ?string
    {
        return $data . ' World!';
    }
}
image-4

At this point, you have what it takes to start coding your own library. As far as I'm concerned, I'm going to create the smallest and simplest possible methods so that I can test them as easily as possible. I quite like the principle of "one method, one responsibility," allowing me to clearly divide what I want to do. Plus, assigning a simple responsibility to each of my methods allows me to include the contract/documentation directly in the name. Thus, a "isValid" method will only have the responsibility of testing if an entry is valid, etc. Here's the schema of the class we're going to use. Again, this is just to illustrate our tutorial.

public isValid(string $data): bool

public fixIfInvalid(string $data):? string

public fix(string $data): string

public fixLength(array $values): string
 
public unserialize(string $data) 

So in order, we call unserialize -> we test if invalid -> we fix -> we deserialize. Very simple indeed. I'm providing the code for the example, and let's move on.

<?php

namespace Partitech\FixUnSerialize;

class UnSerializeService
{
    /**
     * @param string $data
     * @return mixed
     */
    public function unserialize(string $data)
    {
        $data = $this->fixIfInvalid($data);
        return unserialize($data);
    }

    /**
     * @param string $data
     * @return string|null
     */
    public function fixIfInvalid(string $data): ?string
    {
        if (!$this->isValid($data)) {
            $data = $this->fix($data);
        }
        return $data;
    }

    /**
     * @param string $data
     * @return bool
     */
    public function isValid(string $data): bool
    {
        if (!@unserialize($data)) {
            return false;
        }

        return true;
    }

    /**
     * @param string $data
     * @return string
     */
    public function fix(string $data): string
    {
        $pattern = '/s\:(\d+)\:\"(.*?)\";/s';
        return preg_replace_callback($pattern, [$this, 'fixLength'], $data);
    }

    /**
     * @param array $values
     * @return string
     */
    public function fixLength(array $values): string
    {
        $string = $values[2];
        $length = strlen($string);
        return 's:' . $length . ':"' . $string . '";';
    }
}
<?php

namespace Partitech\FixUnSerialize\Tests;

use Partitech\FixUnSerialize\UnSerializeService;
use PHPUnit\Framework\TestCase;

class FixUnSerializeTest extends TestCase
{
    const VALID_STRING = 'a:2:{s:4:"test";s:4:"test";s:5:"test2";s:5:"test2";}';
    const INVALID_STRING = 'a:2:{s:123456:"test";s:4:"test";s:5:"test2";s:5:"test2";}';

    private UnSerializeService $unserializeService;

    public function __construct(?string $name = null, array $data = [], $dataName = '')
    {
        parent::__construct($name, $data, $dataName);
        $this->unserializeService = new UnSerializeService();
    }

    public function testIsValidTrue()
    {
        $this->assertTrue($this->unserializeService->isValid(self::VALID_STRING));
    }

    public function testIsValidFalse()
    {
        $this->assertNotTrue($this->unserializeService->isValid(self::INVALID_STRING));
    }

    public function testFixIfInvalidWithValidString()
    {
        $this->assertEquals(
            self::VALID_STRING,
            $this->unserializeService->fixIfInvalid(self::VALID_STRING)
        );
    }

    public function testFixIfInvalidWithInvalidString()
    {
        $this->assertEquals(
            self::VALID_STRING,
            $this->unserializeService->fixIfInvalid(self::INVALID_STRING)
        );
    }

    public function testFixLength()
    {
        $data = [
            's:5000:"test2";',
            5,
            'test2'
        ];
        $expected = 's:5:"test2";';
        $this->assertEquals(
            $expected,
            $this->unserializeService->fixLength($data)
        );
    }

    public function testFixValid()
    {
        $this->assertEquals(
            self::VALID_STRING,
            $this->unserializeService->fix(self::VALID_STRING)
        );
    }

    public function testFixInvalid()
    {
        $this->assertEquals(
            self::VALID_STRING,
            $this->unserializeService->fix(self::INVALID_STRING)
        );
    }

    public function testUnserializeValid()
    {
        $this->assertEquals(
            unserialize(self::VALID_STRING),
            $this->unserializeService->unserialize(self::VALID_STRING)
        );
    }

    public function testUnserializeInvalid()
    {
        $this->assertEquals(
            unserialize(self::VALID_STRING),
            $this->unserializeService->unserialize(self::INVALID_STRING)
        );
    }
}

Now that we have our code, we're going to push it to Github. We start by versioning our project and adding the list of files that we don't want to include.

git init
touch .gitignore
echo "vendor/" >> .gitignore
echo ".phpunit.result.cache" >> .gitignore
echo ".idea/" >> .gitignore
git add .
git commit -m "First commit"

Initialize an empty repository on Github. We can now push our code to our repository.

git remote add origin ssh://git@github.com/partitech/fix-serialize.git
git branch -M main
git push -f -u origin main

Now that your code is published on Github we can move on.

image-5

Create your first release.

image-6

On the create release page, select "Choose a tag" and enter "v0.0.1" and click on "create new tag", same for the name of your release. Enter "Initial release" as a description and click on "Publish release".

image-8

Once created, you will be on your first release page.

image-9

Our last step will consist of referencing our version on Packagist so that it's directly accessible via Composer.

On the page https://packagist.org/login/ use the option "log with Github".

image-10

Submit your repository:

image-11
image-12
image-13

You now have access to your package directly via:

composer require partitech/fix-unserialize

Awesome 😉

We will now configure Github to notify Packagist of any changes to the repository.

Retrieve your API Token on Packagist https://packagist.org/profile/. Click on Show API Token and copy your key.

image-17

In GitHub, go to Settings > Webhooks > Edit.

image-16

Change the Secret by pasting your Packagist API key.

image-18
image-20

And voila, you can do a push on your repository and check the logs Settings > Webhooks > Recent Deliveries

image-21

Make sure everything is okay on Packagist.

image-22

You now know how to create a Composer library and how to deliver it to the rest of the world 😊