Tuesday, 2 June 2015

PHP: trying (but not succeeding) to create my own Github-based Composer Package

G'day:
So here's one of those articles in which I document trying to do something, but failing miserably. Enjoy.

Update:

This has been sorted out. See the follow-up article: "PHP: Fixed! My colleague sorts that Composer issue out for me".

Composer was a real revelation to me, coming from a CFML world. Package Management! And it's dead easy to use, and just seems to work. Obviously CFML now has CommandBox which sounds like it does similar sort of stuff (and more, unrelated stuff), but it was never around when I was doing CFML so I'm not familiar with it.

We use Composer extensively in our app to pull in all manner of modules. Indeed on most of the code I've been writing about PHP on this blog I've been using Composer to install Silex, Twig, Guzzle, etc. All just via a simple JSON file. This is the one for the project in which I was looking at Guzzle:

{
    "require": {
        "silex/silex": "~1.2",
        "guzzlehttp/guzzle"    : "~5.0",
        "twig/twig": "~1.0"
    },
    "autoload": {
        "psr-4": {
            "me\\adamcameron\\asyncguzzle\\": "src/"
        }
    }
}

That's all straight forward.

I've got some helper code I'd like to bring in as a dependency to a project, and I figured I must be able to use Composer to pull in dependencies from somewhere other than Packagist, it's package repository. And, indeed one can, and the instructions seem fairly easy.

First things first I need a new GitHub repo for my package. I tried to just stick it in a subdirectory of my main "scratch" repo, but Git doesn't understand "directories", it just does repos, so Composer is likewise constrained (it uses the GitHub API to get the files). All I need to make a repo Composer will understand is to add a composer. json file:

{
    "name": "dac/composertest"
}

And then chuck my package code in their too. In this case I'm taking baby steps, so I'm using this basically useless class:

<?php
// Message.php
namespace composertest;
class Message {
    private $message;
    public function __construct($message){
        $this->message = $message;
    }
    
    public function getMessage(){
        return $this->message;
    }
}

Next I add a section to my main project's (the one that will be loading this dependency) composer.json file:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/adamcameron/composertest"
        }
    ],
    "require": {
        "dac/composertest" : "dev-master"
    }
}

This just tells Composer where to look for a non-default repository (this is in addition to the places it looks by default,not instead of ~). And then I also stick in a normal dependency entry, pointing to the package in my GitHub repository. Note that the repo doesn't know about the specific dependencies, it just knows about the GitHub repo. The packages and the dependency on the package is what stitches them together. Also note here I'm being a bit fast and loose with my dependency versioning. Specifying dev-master will mean any composer update calls will just get the current state of the master branch of the repo, which is generally not what one might want... one would want to get a specific version instead, but as I'm only messing around here: this is fine.

All good. Now I run a composer update on my project and...

D:\Websites\php.composer.local>composer update

D:\Websites\php.composer.local>php C:\apps\php\composer.phar update
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing dac/composertest (dev-master 2a0ab1d)
    Cloning 2a0ab1de163f42eaf7c7e0a47c7c9ff09a2bca86

Writing lock file
Generating autoload files

D:\Websites\php.composer.local>


Cool. So that seemed to work. Seemed to work.

Looking at the file system, my files are there:

php.composer.local/
    vendor/
        autoload.php
        dac/
            composertest/
                Message.php
                composer.json

(I've omitted a bunch of composer- and git-specific stuff from that listing).

I have two test files to verify this all works:

// viaFileSystem.php
require __DIR__ . '\..\vendor\dac\composertest\Message.php';
$o = new composertest\Message("G'day World via file system");
echo $o->getMessage();

This is just a baseline to verify the code actually runs. It does:

G'day World via file system

And another version leveraging Composer's autoload file, which should locate and load any classes from dependencies it's loaded for me:

// viaComposer.php
require __DIR__ . '\..\vendor\autoload.php';
use composertest\Message;
$o = new Message("G'day world via Composer");
echo $o->getMessage();

It's important to note that when I write this code in PHPStorm - which knows about my Composer-loaded dependencies - it "knows" about the Message class, doing the auto-completion of the name-spacing, as well as doing code-hinting on the getMessage() method.

However when I run the code, I get this:

Fatal error: Class 'composertest\Message' not found in D:\Websites\php.composer.local\public\viaComposer.php on line 8


So that's not cool.

I wondered if I actually needed the version property in the composertest/composer.json, but that didn't help. I also created the version of composertest in GitHub, and specifically requested that in php.composer.local/composer.json, but that didn't help either. The code downloads and installs fine, but Composer's autoload doesn't find it.

I've googled all over the place, and can't see what I'm doing wrong. The next step is to start wading through the autoloader to find out WTF it's actually doing, and where it's going wrong. I'm not looking forward to this.

Do you wanna know the most frustrating thing? I did actually get it to work once, but then having horsed around with some versioning it stopped working, and then after reverting everything it resume working again. I started from scratch with a different site, different repo, different package, and this didn't work either.

I'm buggered if I know what I've done wrong.

Anyone have any ideas? I'll probably raise a question on Stack Overflow too, I s'pose. Maybe tomorrow.

--
Adam