Moving your PHP code into reusable composer packages

Moving your PHP code into reusable composer packages
Photo by Markus Spiske / Unsplash

Writing code for anything takes time. Time usually spent lifting some components from other projects. After a while, you've probably already got 99% of the initial setup code for the new project elsewhere, floating around in your git repositories. Imagine just reusing this code from a single place in multiple projects. This is exactly the job of a dependency manager.

Every developer has been there. You load your editor for your new project, load the editor for an old project, and painstakingly start copying and pasting code.

Let's say you've got a class within your code, and it's complicated. You don't want to rewrite it again. Likewise, you know if there's a bug you'll likely need to fix it across several projects. This is the perfect use case for packaging it up for Composer.

What is Composer?

Composer is a dependency manager system for PHP.  It's a way of forcing a project to use a very specific set of libraries at very specific points in their development history. For example, you could force a library called SomeLib to use exactly version 1.2.345. It's very much similar to NPM for Node.js and Gradle for Java.

Right, but how do we get started with it?

First things first, Composer can either use public repositories (you'll find these on Packagist), or alternatively it can also use private repositories.  We'll focus on private repositories for this exercise.  There's a full working example of this in our GitHub organisation that you can refer to if you get stuck.

Let's imagine you have this class below and it's very complicated logic (I know, it's pretty basic looking, but bear with me!). Now, you've written it, and it's beautiful. So beautiful in fact that you're proud of yourself for writing something so great. Take a picture, send it around to your friends, boast on Twitter. Celebrate it!

<?php
namespace MyLibraryExample;

/**
 * @class MySuperClass
 * @package MyLibraryExample
 */
class MySuperClass
{
    /**
     * @var string
     */
    private string $name;

    /**
     * @param string $name
     */
    public function __construct(string $name)
    {
        $this->name = $name;
    }
    
    /**
     * @return string
     */
    public function hello(): string
    {
        // Maybe just pretend this is more 
        // complicated than it actually is.
        return $this->name;
    }
}
A very simple example

Initially we want an entirely empty directory. It doesn't really matter what you call it, just make a directory, somewhere.

Within that directory we're going to need a folder called src and a file called composer.json

The directory should look like this now:

src/
composer.json
The basic directory structure

Let's copy the class from above into the src/ directory. The structure will now look like this:

src/
src/MySuperClass.php
composer.json

Next we need to edit that composer.json file. We'll start with the basic working version and get into the more complicated controls afterwards.  Add this content to your empty composer.json file, changing the value of the "name" section to be your-git-username/your-package-name. You might end up with something like...

{
  "name": "district-5/blog-example-composer-package",
  "description": "This is a description",
  "authors": [
    {
      "name": "My Name",
      "email": "my.name@example.com"
    }
  ],
  "autoload": {
    "psr-4": {
      "MyLibraryExample\\": "src/"
    }
  }
}
the package composer.json file

For our example, we're using the package name of district-5/blog-example-composer-package because our GitHub username is district-5 and the repository is called blog-example-composer-package (see this repository). This just makes things easier to translate, although you can name them anything you want.

We can give this a quick test. Assuming you have Composer set up (if not, visit here and install Composer before returning to the instructions).

In your terminal (or shell), run:

$ cd the-library-path
$ composer install
installing the package to test

Composer will output the following.  If there's an error, or something went wrong, look back at the examples above and make sure you've not made a mistake somewhere.

No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file.
Loading composer repositories with package information
Updating dependencies
Nothing to modify in lock file
Writing lock file
Installing dependencies from lock file (including require-dev)
Nothing to install, update or remove
Generating autoload files
me@my-mac $
installing the package for the first time

If it looks the same as the above, we can move on. You'll likely notice that there are some new items in the project tree.  Specifically, a new composer.lock file which contains all of the data needed for composer to reinstall the package, and a folder called vendor.  The vendor folder will contain the all-important autoload.php. The autoload.php file contains the class mappings to automatically load a class when you ask for it.

Let's give it a quick test...

In the root directory (the same directory as your composer.json file we'll create a test.php file. Don't worry about this too much. We'll be removing it again in a second. Within this file you'll need the following content...

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

$instance = new \MyLibraryExample\MySuperClass('world');
var_dump($instance->hello());
quick test file content for testing our work so far

In your terminal, within the directory containing the test.php file, we'll run a quick command to check the output.

$ cd the-library-path
$ php test.php
creating a test.php file

Hopefully the output from this is string(5) "world".  If it is, great! If it's not, it's likely you've changed either the namespace in the PHP file, or the namespace in the "autoload" section of the composer.json file.

We're finished with the test.php file, so you can delete it now.

At this point, you've got everything you need to start packaging, so let's move on to the finer tuning.  If you'd rather stay at this point, you can skip to the "Let's get packaging" section below.

The example class we used here has a typed property, which means it will 'require' at least PHP version 7.4 to work.  We can add this to a new "require" section within the Composer file.  Add this section to the middle of the composer file (I generally place it above the "autoload" section).

{
  "name": "district-5/blog-example-composer-package",
  "description": "This is a description",
  "authors": [
    {
      "name": "My Name",
      "email": "my.name@example.com"
    }
  ],
  "require": {
    "php": ">=7.4"
  },
  "autoload": {
    "psr-4": {
      "MyLibraryExample\\": "src/"
    }
  }
}
requiring at least PHP version 7.4 in the composer.json file

What we've actually done now is prevent any projects using less than PHP 7.4 from using this library. Nice 😎

Let's get packaging!

We're using git for this example, so to prevent the repository being filled with files we don't need, we'll create a .gitignore file.

$ cd the-library-path
$ touch .gitignore
create a .gitignore file

In this file, we'll be excluding files from git that we just don't need. For example, on a Mac you'll want to ignore .DS_Store specifically, but for Windows there are files such as the Thumbs.db file that you'll want to also exclude.  GitHub runs an excellent repository of .gitignore files here, and we've included out .gitignore file in our repository for this example.

For the basic example here let's just add the basics. Add the next lines into your .gitignore file...

composer.lock
vendor/

# If you don't have composer globally installed also add this...
composer.phar
suggested minimum .gitignore file.

At this point, the only paths that will get committed to git are:

src/
src/MySuperClass.php
composer.json
.gitignore
the directory structure at this point

Let's initialise the repository and push it to your origin repository...

$ cd the-library-path
$ git init
Initialized empty Git repository in /Users/name/my-code/the-library-path/.git/
initialising the git repository

Let's check which files are pending...

$ cd the-library-path
$ git status

On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .gitignore
        composer.json
        src/

nothing added to commit but untracked files present (use "git add" to track)
check which files should be staged for commit

Add these files to staging...

$ cd the-library-path
$ git add .
$ git status

On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   .gitignore
        new file:   composer.json
        new file:   src/MySuperClass.php

stage the files for commit

Commit the files...

$ cd the-library-path
$ git commit -m "My first commit"

[master (root-commit) 3ec33a3] My first commit
 3 files changed, 132 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 composer.json
 create mode 100644 src/MySuperClass.php
making your first library commit

Assuming you have a git repository set up already, let's add the origin and push. Remember to change the git repository link below

$ cd the-library-path
$ git remote add origin git@github.com:district-5/blog-example-composer-package.git
adding the git origin for our library

Now we need to push that commit into the new repository. We can do this by issuing...

$ cd the-library-path
$ git push -u origin master 

Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 4 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 1.99 KiB | 1.99 MiB/s, done.
Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:district-5/blog-example-composer-package.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.
pushing the commit to git for the first time

Amazing. But let's create a tag of this code also. By doing so we lock in the version numbers. We'll start with 0.0.1. After we've created the tag, we'll also push it.

$ cd the-library-path
$ git tag -a 0.0.1 -m "My first version"
$ git push origin 0.0.1

Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 167 bytes | 167.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:district-5/blog-example-composer-package.git
 * [new tag]         0.0.1 -> 0.0.1
creating and pushing a tag

Using the tagged version...

We'll leave the library now. Let's go into a project directory and create (or edit) a composer.json file.

Because in this example we're using a a private repository, we'll need to tell the project level composer.json (not the library file from above) where to find the the code. But if you're using a public repository, and you've set things up with Packagist, you can just omit the "repositories" section entirely.

{
    "repositories":[
        {
            "type": "vcs",
            "url": "git@github.com:district-5/blog-example-composer-package.git"
        }
    ],
    "require": {
        "district-5/blog-example-composer-package": "0.0.1"
    }
}
project level composer.json file

What we've done now is tell the project Composer where to find our district-5/blog-example-composer-package and to always use version 0.0.1. You can alter the value from "0.0.1" to be >=0.0.1 if the required version should be at least 0.0.1.

At this point, as long as your project references the vendor/autoload.php file before usage of the library class you'll be reusing code!

Updating the library...

Let's say you've just made a change to the library and you want to update it in the project.  Go ahead and change your library code, commit and push the change to master. Next you'll want to create the tag using the git tag -a 0.0.2 -m 0.0.2 and push it with git push origin 0.0.2.

Load the project (not the library).  If you used 0.0.1 as the version in the project you'll edit the project level composer.json file now to contain the following, but if you've used >=0.0.1 you can skip this next snippet...

{
    "repositories":[
        {
            "type": "vcs",
            "url": "git@github.com:district-5/blog-example-composer-package.git"
        }
    ],
    "require": {
        "district-5/blog-example-composer-package": "0.0.2"
    }
}
updated version in the project level composer.json file

Next we'll update just this library. Be extra careful with this one. If you ran a global update you'd likely break another dependency.

Within the project directory (not the library)...

$ cd the-project-path
$ composer update district-5/blog-example-composer-package
updating the project to version 0.0.2

There you have it. Your project is now using version 0.0.2 of the library.

Good to know...

Projects are slightly different to libraries.  Typically speaking, a library should not contain a composer.lock file in the git history, and neither a project or library should ever have the vendor directory committed. The versions required should be dictated by your project, although there are use cases where you may want to store the lock file for the library also.

Here at District5 we don't believe in writing the same code twice. By keeping clean libraries of code, we're able to vastly decrease development costs for our customers by reusing instead of rewriting.

We're incredibly proud to be proactive about squashing bugs and rolling out bug fixes for our customers in a timely fashion.  Packaging plays a huge part in our business and helps streamline development.